From 843beddbf339cf3d5ef9fc459d8b5e94e74435dd Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Fri, 27 Sep 2024 18:07:07 -0700 Subject: [PATCH 01/73] Target network fees is in IRON (#5455) Previously, we showed this fee as the token being sent to the target network. This was an incorrect assumption on our part. The fee is actually taken in IRON. The chainport fee is the one that is always either in the target token or portx token. --- ironfish-cli/src/commands/wallet/chainport/send.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/chainport/send.ts b/ironfish-cli/src/commands/wallet/chainport/send.ts index bcce482551..ef3095c33c 100644 --- a/ironfish-cli/src/commands/wallet/chainport/send.ts +++ b/ironfish-cli/src/commands/wallet/chainport/send.ts @@ -342,12 +342,7 @@ export class BridgeCommand extends IronfishCommand { ) const ironfishNetworkFee = CurrencyUtils.render(raw.fee, true) - const targetNetworkFee = CurrencyUtils.render( - BigInt(txn.gas_fee_output.amount), - true, - asset.web3_address, - assetData.verification, - ) + const targetNetworkFee = CurrencyUtils.render(BigInt(txn.gas_fee_output.amount), true) let chainportFee: string From 5d396588299982def90d72c76ce8fe80df2d88fa Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Sat, 28 Sep 2024 21:48:51 -0400 Subject: [PATCH 02/73] Fixes fetching assets for display in wallet:transactions:info (#5456) --- ironfish-cli/src/commands/wallet/transactions/info.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ironfish-cli/src/commands/wallet/transactions/info.ts b/ironfish-cli/src/commands/wallet/transactions/info.ts index ee816378ef..a80b0b2f24 100644 --- a/ironfish-cli/src/commands/wallet/transactions/info.ts +++ b/ironfish-cli/src/commands/wallet/transactions/info.ts @@ -122,6 +122,7 @@ export class TransactionInfoCommand extends IronfishCommand { for (const note of transaction.notes) { const asset = await client.wallet.getAsset({ + account: account, id: note.assetId, }) From 652b385c43abc19d7afb07801bd884113682f0d8 Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Mon, 30 Sep 2024 13:00:21 -0400 Subject: [PATCH 03/73] Fix wallet:chainport:send fee selection when -a is used (#5458) --- ironfish-cli/src/commands/wallet/chainport/send.ts | 1 + ironfish-cli/src/utils/fees.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ironfish-cli/src/commands/wallet/chainport/send.ts b/ironfish-cli/src/commands/wallet/chainport/send.ts index ef3095c33c..1f6a37b4e5 100644 --- a/ironfish-cli/src/commands/wallet/chainport/send.ts +++ b/ironfish-cli/src/commands/wallet/chainport/send.ts @@ -311,6 +311,7 @@ export class BridgeCommand extends IronfishCommand { if (params.fee === null && params.feeRate === null) { rawTransaction = await selectFee({ client, + account: from, transaction: params, logger: this.logger, }) diff --git a/ironfish-cli/src/utils/fees.ts b/ironfish-cli/src/utils/fees.ts index cda295d36e..fce9b08088 100644 --- a/ironfish-cli/src/utils/fees.ts +++ b/ironfish-cli/src/utils/fees.ts @@ -20,7 +20,7 @@ import { promptCurrency } from './currency' export async function selectFee(options: { client: Pick transaction: CreateTransactionRequest - account?: string + account: string | undefined confirmations?: number logger: Logger }): Promise { From 8d9c8e2a9d9bc9531217533400c5851790fa8f84 Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Tue, 1 Oct 2024 12:17:44 -0400 Subject: [PATCH 04/73] Use Iron Fish API for bridge info (#5457) * Fixes fetching assets for display in wallet:transactions:info * Use Iron Fish API for bridge info * Enable network icon * Remove commented fields * Change "bridge transaction fees" to "bridge transaction details" Co-authored-by: Rahul Patni --------- Co-authored-by: Rahul Patni --- .../src/commands/wallet/chainport/send.ts | 62 +++++++++---------- .../src/commands/wallet/transactions/info.ts | 9 ++- ironfish-cli/src/utils/chainport/config.ts | 6 +- ironfish-cli/src/utils/chainport/requests.ts | 57 ++++++++--------- ironfish-cli/src/utils/chainport/types.ts | 10 +-- ironfish-cli/src/utils/chainport/utils.ts | 16 +++-- 6 files changed, 75 insertions(+), 85 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/chainport/send.ts b/ironfish-cli/src/commands/wallet/chainport/send.ts index 1f6a37b4e5..210386ab7f 100644 --- a/ironfish-cli/src/commands/wallet/chainport/send.ts +++ b/ironfish-cli/src/commands/wallet/chainport/send.ts @@ -21,10 +21,10 @@ import * as ui from '../../../ui' import { ChainportBridgeTransaction, ChainportNetwork, - ChainportVerifiedToken, + ChainportToken, fetchChainportBridgeTransaction, - fetchChainportNetworkMap, - fetchChainportVerifiedTokens, + fetchChainportTokenPaths, + fetchChainportTokens, } from '../../../utils/chainport' import { isEthereumAddress } from '../../../utils/chainport/address' import { promptCurrency } from '../../../utils/currency' @@ -182,7 +182,9 @@ export class BridgeCommand extends IronfishCommand { this.error('Expiration sequence must be non-negative') } - const tokens = await fetchChainportVerifiedTokens(networkId) + ux.action.start('Fetching bridgeable assets') + const tokens = await fetchChainportTokens(networkId) + ux.action.stop() const tokenNames = tokens.map( (t, index) => `${index + 1}. ${t.name} (${t.symbol}) - ${t.web3_address}`, @@ -211,9 +213,7 @@ export class BridgeCommand extends IronfishCommand { assetId = asset.id } - const asset: ChainportVerifiedToken | undefined = tokens.find( - (t) => t.web3_address === assetId, - ) + const asset: ChainportToken | undefined = tokens.find((t) => t.web3_address === assetId) if (!asset) { this.logger.error( @@ -224,8 +224,6 @@ export class BridgeCommand extends IronfishCommand { this.exit(1) } - const targetNetworks = asset.target_networks - const assetData = ( await client.wallet.getAsset({ account: from, @@ -239,7 +237,7 @@ export class BridgeCommand extends IronfishCommand { assetData.verification.status = 'verified' } - const targetNetwork = await this.selectNetwork(networkId, targetNetworks, asset) + const targetNetwork = await this.selectNetwork(networkId, asset) let amount if (flags.amount) { @@ -280,12 +278,20 @@ export class BridgeCommand extends IronfishCommand { from: string, to: string, amount: bigint, - asset: ChainportVerifiedToken, + asset: ChainportToken, assetData: RpcAsset, ) { const { flags } = await this.parse(BridgeCommand) - const txn = await fetchChainportBridgeTransaction(networkId, amount, to, network, asset) + ux.action.start('Fetching bridge transaction details') + const txn = await fetchChainportBridgeTransaction( + networkId, + amount, + asset.web3_address, + network.chainport_network_id, + to, + ) + ux.action.stop() const params: CreateTransactionRequest = { account: from, @@ -321,7 +327,7 @@ export class BridgeCommand extends IronfishCommand { rawTransaction = RawTransactionSerde.deserialize(bytes) } - this.displayTransactionSummary(txn, rawTransaction, from, to, asset, assetData, network) + this.displayTransactionSummary(txn, rawTransaction, from, to, assetData, network) return rawTransaction } @@ -331,14 +337,13 @@ export class BridgeCommand extends IronfishCommand { raw: RawTransaction, from: string, to: string, - asset: ChainportVerifiedToken, assetData: RpcAsset, network: ChainportNetwork, ) { const bridgeAmount = CurrencyUtils.render( BigInt(txn.bridge_output.amount) - BigInt(txn.bridge_fee.source_token_fee_amount ?? 0), true, - asset.web3_address, + assetData.id, assetData.verification, ) const ironfishNetworkFee = CurrencyUtils.render(raw.fee, true) @@ -363,7 +368,7 @@ export class BridgeCommand extends IronfishCommand { chainportFee = CurrencyUtils.render( BigInt(txn.bridge_fee.source_token_fee_amount ?? 0), true, - asset.web3_address, + assetData.id, assetData.verification, ) } @@ -373,7 +378,7 @@ export class BridgeCommand extends IronfishCommand { From ${from} To ${to} - Target Network ${network.name} + Target Network ${network.label} Estimated Amount Received ${bridgeAmount} Fees: @@ -390,25 +395,13 @@ export class BridgeCommand extends IronfishCommand { private async selectNetwork( networkId: number, - targetNetworks: number[], - asset: ChainportVerifiedToken, + asset: ChainportToken, ): Promise { ux.action.start('Fetching available networks') - const networks = await fetchChainportNetworkMap(networkId) + const networks = await fetchChainportTokenPaths(networkId, asset.id) ux.action.stop() - const choices = Object.keys(networks).map((key) => { - return { - name: networks[key].label, - value: networks[key], - } - }) - - const filteredChoices = choices.filter((choice) => - targetNetworks.includes(choice.value.chainport_network_id), - ) - - if (filteredChoices.length === 0) { + if (networks.length === 0) { this.error(`No networks available for token ${asset.symbol} on Chainport`) } @@ -419,7 +412,10 @@ export class BridgeCommand extends IronfishCommand { name: 'selection', message: `Select the network you would like to bridge ${asset.symbol} to`, type: 'list', - choices: filteredChoices, + choices: networks.map((network) => ({ + name: network.label, + value: network, + })), }, ]) diff --git a/ironfish-cli/src/commands/wallet/transactions/info.ts b/ironfish-cli/src/commands/wallet/transactions/info.ts index a80b0b2f24..7b6be0d108 100644 --- a/ironfish-cli/src/commands/wallet/transactions/info.ts +++ b/ironfish-cli/src/commands/wallet/transactions/info.ts @@ -16,7 +16,7 @@ import * as ui from '../../../ui' import { displayChainportTransactionSummary, extractChainportDataFromTransaction, - fetchChainportNetworkMap, + fetchChainportNetworks, getAssetsByIDs, useAccount, } from '../../../utils' @@ -100,14 +100,17 @@ export class TransactionInfoCommand extends IronfishCommand { this.log(`\n---Chainport Bridge Transaction Summary---\n`) ux.action.start('Fetching network details') - const chainportNetworks = await fetchChainportNetworkMap(networkId) + const chainportNetworks = await fetchChainportNetworks(networkId) + const network = chainportNetworks.find( + (n) => n.chainport_network_id === chainportTxnDetails.chainportNetworkId, + ) ux.action.stop() await displayChainportTransactionSummary( networkId, transaction, chainportTxnDetails, - chainportNetworks[chainportTxnDetails.chainportNetworkId], + network, this.logger, ) } diff --git a/ironfish-cli/src/utils/chainport/config.ts b/ironfish-cli/src/utils/chainport/config.ts index b8405e283f..5a6ffbb802 100644 --- a/ironfish-cli/src/utils/chainport/config.ts +++ b/ironfish-cli/src/utils/chainport/config.ts @@ -6,8 +6,7 @@ import { MAINNET, TESTNET } from '@ironfish/sdk' const config = { [TESTNET.id]: { - chainportId: 22, - endpoint: 'https://preprod-api.chainport.io', + endpoint: 'https://testnet.api.ironfish.network/', outgoingAddresses: new Set([ '06102d319ab7e77b914a1bd135577f3e266fd82a3e537a02db281421ed8b3d13', 'db2cf6ec67addde84cc1092378ea22e7bb2eecdeecac5e43febc1cb8fb64b5e5', @@ -18,8 +17,7 @@ const config = { ]), }, [MAINNET.id]: { - chainportId: 22, - endpoint: 'https://api.chainport.io', + endpoint: 'https://api.ironfish.network/', outgoingAddresses: new Set([ '576ffdcc27e11d81f5180d3dc5690294941170d492b2d9503c39130b1f180405', '7ac2d6a59e19e66e590d014af013cd5611dc146e631fa2aedf0ee3ed1237eebe', diff --git a/ironfish-cli/src/utils/chainport/requests.ts b/ironfish-cli/src/utils/chainport/requests.ts index 11124fe83c..558e0836c5 100644 --- a/ironfish-cli/src/utils/chainport/requests.ts +++ b/ironfish-cli/src/utils/chainport/requests.ts @@ -1,14 +1,13 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { MAINNET } from '@ironfish/sdk' import axios from 'axios' import { getConfig } from './config' import { ChainportBridgeTransaction, ChainportNetwork, + ChainportToken, ChainportTransactionStatus, - ChainportVerifiedToken, } from './types' // Wrappers around chainport API requests. Documentation here: https://docs.chainport.io/for-developers/integrate-chainport/iron-fish/iron-fish-to-evm @@ -18,50 +17,52 @@ export const fetchChainportTransactionStatus = async ( hash: string, ): Promise => { const config = getConfig(networkId) - const url = `${config.endpoint}/api/port?base_tx_hash=${hash}&base_network_id=${config.chainportId}` + const url = new URL(`/bridges/transactions/status`, config.endpoint) + url.searchParams.append('hash', hash) - return await makeChainportRequest(url) + return await makeChainportRequest(url.toString()) } -export const fetchChainportNetworkMap = async ( +export const fetchChainportNetworks = async ( networkId: number, -): Promise<{ [key: string]: ChainportNetwork }> => { +): Promise => { const config = getConfig(networkId) - const url = `${config.endpoint}/meta` + const url = new URL('/bridges/networks', config.endpoint).toString() - return ( - await makeChainportRequest<{ cp_network_ids: { [key: string]: ChainportNetwork } }>(url) - ).cp_network_ids + return (await makeChainportRequest<{ data: ChainportNetwork[] }>(url)).data } -export const fetchChainportVerifiedTokens = async ( - networkId: number, -): Promise => { +export const fetchChainportTokens = async (networkId: number): Promise => { const config = getConfig(networkId) - let url - if (networkId === MAINNET.id) { - url = `${config.endpoint}/token/list?network_name=IRONFISH` - } else { - url = `${config.endpoint}/token_list?network_name=IRONFISH` - } + const url = new URL('/bridges/tokens', config.endpoint).toString() + + return (await makeChainportRequest<{ data: ChainportToken[] }>(url)).data +} - return (await makeChainportRequest<{ verified_tokens: ChainportVerifiedToken[] }>(url)) - .verified_tokens +export const fetchChainportTokenPaths = async ( + networkId: number, + tokenId: number, +): Promise => { + const config = getConfig(networkId) + const url = new URL(`/bridges/tokens/${tokenId}/networks`, config.endpoint).toString() + return (await makeChainportRequest<{ data: ChainportNetwork[] }>(url)).data } export const fetchChainportBridgeTransaction = async ( networkId: number, amount: bigint, - to: string, - network: ChainportNetwork, - asset: ChainportVerifiedToken, + assetId: string, + targetNetworkId: number, + targetAddress: string, ): Promise => { const config = getConfig(networkId) - const url = `${config.endpoint}/ironfish/metadata?raw_amount=${amount.toString()}&asset_id=${ - asset.web3_address - }&target_network_id=${network.chainport_network_id.toString()}&target_web3_address=${to}` + const url = new URL(`/bridges/transactions/create`, config.endpoint) + url.searchParams.append('amount', amount.toString()) + url.searchParams.append('asset_id', assetId) + url.searchParams.append('target_network_id', targetNetworkId.toString()) + url.searchParams.append('target_address', targetAddress.toString()) - return await makeChainportRequest(url) + return await makeChainportRequest(url.toString()) } const makeChainportRequest = async (url: string): Promise => { diff --git a/ironfish-cli/src/utils/chainport/types.ts b/ironfish-cli/src/utils/chainport/types.ts index b0d40cddfb..7aaaee1cfc 100644 --- a/ironfish-cli/src/utils/chainport/types.ts +++ b/ironfish-cli/src/utils/chainport/types.ts @@ -25,25 +25,19 @@ export type ChainportBridgeTransaction = { export type ChainportNetwork = { chainport_network_id: number - shortname: string - name: string - chain_id: number explorer_url: string label: string - blockchain_type: string - native_token_symbol: string network_icon: string } -export type ChainportVerifiedToken = { - decimals: number +export type ChainportToken = { id: number + decimals: number name: string pinned: boolean web3_address: string symbol: string token_image: string - target_networks: number[] chain_id: number | null network_name: string network_id: number diff --git a/ironfish-cli/src/utils/chainport/utils.ts b/ironfish-cli/src/utils/chainport/utils.ts index f44474936b..c868bd7ce7 100644 --- a/ironfish-cli/src/utils/chainport/utils.ts +++ b/ironfish-cli/src/utils/chainport/utils.ts @@ -119,7 +119,7 @@ export const displayChainportTransactionSummary = async ( if (data.type === TransactionType.RECEIVE) { logger.log(` Direction: Incoming -Source Network: ${network.name} +Source Network: ${network.label} Address: ${data.address} Explorer Account: ${network.explorer_url + 'address/' + data.address} Target (Ironfish) Network: ${defaultNetworkName(networkId)}`) @@ -134,7 +134,7 @@ Source Network: ${defaultNetworkName(networkId)} Transaction Status: ${transaction.status} Transaction Hash: ${transaction.hash} ============================================== -Target Network: ${network.name} +Target Network: ${network.label} Address: ${data.address} Explorer Account: ${network.explorer_url + 'address/' + data.address}` @@ -146,7 +146,6 @@ Target Network: ${network.name} ux.action.start('Fetching transaction information on target network') const transactionStatus = await fetchChainportTransactionStatus(networkId, transaction.hash) - logger.log(`Transaction status fetched`) ux.action.stop() logger.log(basicInfo) @@ -159,12 +158,11 @@ If this issue persists, please contact chainport support: https://helpdesk.chain return } - if (!transactionStatus.base_tx_hash || !transactionStatus.base_tx_status) { - logger.log(` Transaction Status: pending`) - return - } - - if (transactionStatus.target_tx_hash === null) { + if ( + !transactionStatus.base_tx_hash || + !transactionStatus.base_tx_status || + !transactionStatus.target_tx_hash + ) { logger.log(` Transaction Status: pending`) return } From cac1617d78e576b95f46f94a867359ee00c96acc Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Tue, 1 Oct 2024 10:57:54 -0700 Subject: [PATCH 05/73] Fix vscode not resolving test types (#5294) With some recent changes to our jest types, vscode no longer resolved `describe`, `expect`, etc. I believe the issue is that vscode's typescript extension relies on `tsconfig.json`, without giving users the ability to change that. Our default tsconfig.json excluded our test files, which is what was causing the issue. I do not know why this wasn't an issue when we used `@types/jest`, but there seems to be a lot of special-casing around `@types`, so maybe it was a defualt tsconfig option somewhere that I wasn't able to figure out. In the mean time, if we flip our usage of tsconfig - make tsconfig.json the one that includes all tests files, and tsconfig.build.json the one we use for building the actual deliverable code, we allow vscode to do what it wants to do, while hopefully still only building the files we want --- ironfish-cli/package.json | 11 +++++---- ironfish-cli/tsconfig.build.json | 14 +++++++++++ ironfish-cli/tsconfig.json | 10 +++----- ironfish-cli/tsconfig.test.json | 9 ------- ironfish/package.json | 24 +++++++++---------- ...tsconfig.test.json => tsconfig.build.json} | 4 ++-- ironfish/tsconfig.json | 4 ++-- 7 files changed, 39 insertions(+), 37 deletions(-) create mode 100644 ironfish-cli/tsconfig.build.json delete mode 100644 ironfish-cli/tsconfig.test.json rename ironfish/{tsconfig.test.json => tsconfig.build.json} (60%) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index cfff8cf806..84ff196ead 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -43,16 +43,17 @@ "yarn": "^1.22.10" }, "scripts": { - "build": "tsc -b", - "lint": "tsc -b && eslint --ext .ts,.tsx,.js,.jsx src/", + "build": "tsc -b tsconfig.build.json", + "build:tests": "tsc -b", + "lint": "yarn build && eslint --ext .ts,.tsx,.js,.jsx src/", "lint:deprecated": "yarn lint --rule \"deprecation/deprecation: warn\"", - "lint:fix": "tsc -b && eslint --ext .ts,.tsx,.js,.jsx src/ --fix", + "lint:fix": "yarn build && eslint --ext .ts,.tsx,.js,.jsx src/ --fix", "start:dev": "node start", "start": "yarn build && yarn start:js", "start:js": "cross-env OCLIF_TS_NODE=0 IRONFISH_DEBUG=1 node --expose-gc --inspect=:0 --inspect-publish-uid=http --enable-source-maps bin/run", "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest", - "test:coverage:html": "tsc -b tsconfig.test.json && cross-env NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns --coverage --coverage-reporters html", - "test:watch": "yarn clean && tsc -b && tsc -b tsconfig.test.json && cross-env NODE_OPTIONS=--experimental-vm-modules jest --watch --coverage false", + "test:coverage:html": "yarn build:tests && cross-env NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns --coverage --coverage-reporters html", + "test:watch": "yarn clean && yarn build && yarn build:tests && cross-env NODE_OPTIONS=--experimental-vm-modules jest --watch --coverage false", "postpack": "rimraf oclif.manifest.json", "clean": "rimraf build", "prepack": "rimraf build && yarn build && oclif manifest && oclif readme", diff --git a/ironfish-cli/tsconfig.build.json b/ironfish-cli/tsconfig.build.json new file mode 100644 index 0000000000..f47380057d --- /dev/null +++ b/ironfish-cli/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "../config/tsconfig.base.json", + "compilerOptions": { + "lib": ["es2020"], + "outDir": "build", + "rootDir": "./", + "tsBuildInfoFile": "./build/tsconfig.tsbuildinfo" + }, + "include": ["src", "./package.json"], + "exclude": ["src/**/*.test.*"], + "references": [ + { "path": "../ironfish/tsconfig.build.json" }, + ] +} diff --git a/ironfish-cli/tsconfig.json b/ironfish-cli/tsconfig.json index ad0746eaa9..b0454b1a42 100644 --- a/ironfish-cli/tsconfig.json +++ b/ironfish-cli/tsconfig.json @@ -2,12 +2,8 @@ "extends": "../config/tsconfig.base.json", "compilerOptions": { "lib": ["es2020"], - "outDir": "build", - "rootDir": "./", - "tsBuildInfoFile": "./build/tsconfig.tsbuildinfo" + "noEmit": true }, - "include": ["src", "./package.json"], - "references": [ - { "path": "../ironfish" }, - ] + "include": ["src", "package.json"], + "references": [{ "path": "../ironfish/tsconfig.build.json" }] } diff --git a/ironfish-cli/tsconfig.test.json b/ironfish-cli/tsconfig.test.json deleted file mode 100644 index df85e8e540..0000000000 --- a/ironfish-cli/tsconfig.test.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../config/tsconfig.base.json", - "compilerOptions": { - "lib": ["es2020"], - "noEmit": true - }, - "include": [], - "references": [{ "path": "../ironfish" }] -} diff --git a/ironfish/package.json b/ironfish/package.json index 8c68235061..17494b0aba 100644 --- a/ironfish/package.json +++ b/ironfish/package.json @@ -52,18 +52,18 @@ "yup": "0.29.3" }, "scripts": { - "build": "tsc -b", - "build:watch": "tsc -b -w", - "build:tests": "tsc -b tsconfig.test.json", - "lint": "tsc -b && tsc -b tsconfig.test.json && eslint --ext .ts,.tsx,.js,.jsx src/", - "lint:fix": "tsc -b && tsc -b tsconfig.test.json && eslint --ext .ts,.tsx,.js,.jsx src/ --fix", - "start": "tsc -b -w", - "test": "tsc -b && tsc -b tsconfig.test.json && cross-env NODE_OPTIONS=--experimental-vm-modules RUST_BACKTRACE=1 jest --testTimeout=${JEST_TIMEOUT:-5000}", - "test:slow": "tsc -b && tsc -b tsconfig.test.json && cross-env NODE_OPTIONS=--experimental-vm-modules TEST_INIT_RUST=true RUST_BACKTRACE=1 jest --testMatch \"**/*.test.slow.ts\" --testPathIgnorePatterns --testTimeout=${JEST_TIMEOUT:-60000}", - "test:perf": "tsc -b && tsc -b tsconfig.test.json && cross-env NODE_OPTIONS=--experimental-vm-modules TEST_INIT_RUST=true jest --testMatch \"**/*.test.perf.ts\" --testPathIgnorePatterns --testTimeout=${JEST_TIMEOUT:-600000} --runInBand", - "test:perf:report": "tsc -b && tsc -b tsconfig.test.json && cross-env NODE_OPTIONS=--experimental-vm-modules TEST_INIT_RUST=true GENERATE_TEST_REPORT=true jest --config jest.config.js --testMatch \"**/*.test.perf.ts\" --testPathIgnorePatterns --testTimeout=${JEST_TIMEOUT:-600000} --ci", - "test:coverage:html": "tsc -b tsconfig.test.json && cross-env NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns --coverage --coverage-reporters html", - "test:watch": "tsc -b tsconfig.test.json && cross-env NODE_OPTIONS=--experimental-vm-modules jest --watch --coverage false", + "build": "tsc -b tsconfig.build.json", + "build:tests": "tsc -b", + "build:watch": "tsc -b -w tsconfig.build.json", + "lint": "yarn build && yarn build:tests && eslint --ext .ts,.tsx,.js,.jsx src/", + "lint:fix": "yarn build && yarn build:tests && eslint --ext .ts,.tsx,.js,.jsx src/ --fix", + "start": "tsc -b -w tsconfig.build.json", + "test": "yarn build && yarn build:tests && cross-env NODE_OPTIONS=--experimental-vm-modules RUST_BACKTRACE=1 jest --testTimeout=${JEST_TIMEOUT:-5000}", + "test:slow": "yarn build && yarn build:tests && cross-env NODE_OPTIONS=--experimental-vm-modules TEST_INIT_RUST=true RUST_BACKTRACE=1 jest --testMatch \"**/*.test.slow.ts\" --testPathIgnorePatterns --testTimeout=${JEST_TIMEOUT:-60000}", + "test:perf": "yarn build && yarn build:tests && cross-env NODE_OPTIONS=--experimental-vm-modules TEST_INIT_RUST=true jest --testMatch \"**/*.test.perf.ts\" --testPathIgnorePatterns --testTimeout=${JEST_TIMEOUT:-600000} --runInBand", + "test:perf:report": "yarn build && yarn build:tests && cross-env NODE_OPTIONS=--experimental-vm-modules TEST_INIT_RUST=true GENERATE_TEST_REPORT=true jest --config jest.config.js --testMatch \"**/*.test.perf.ts\" --testPathIgnorePatterns --testTimeout=${JEST_TIMEOUT:-600000} --ci", + "test:coverage:html": "yarn build:tests && cross-env NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns --coverage --coverage-reporters html", + "test:watch": "yarn build:tests && cross-env NODE_OPTIONS=--experimental-vm-modules jest --watch --coverage false", "fixtures:regenerate": "find . -name \"__fixtures__\" | xargs rm -rf && NODE_OPTIONS=--experimental-vm-modules JEST_TIMEOUT=1000000000 yarn run test && NODE_OPTIONS=--experimental-vm-modules JEST_TIMEOUT=1000000000 yarn run test:slow && NODE_OPTIONS=--experimental-vm-modules JEST_TIMEOUT=1000000000 yarn run test:perf" }, "devDependencies": { diff --git a/ironfish/tsconfig.test.json b/ironfish/tsconfig.build.json similarity index 60% rename from ironfish/tsconfig.test.json rename to ironfish/tsconfig.build.json index 26971efaa2..1e74446feb 100644 --- a/ironfish/tsconfig.test.json +++ b/ironfish/tsconfig.build.json @@ -1,8 +1,8 @@ { "extends": "../config/tsconfig.base.json", "compilerOptions": { - "noEmit": true, - "tsBuildInfoFile": "./build/tsconfig.tsbuildinfo" + "outDir": "build" }, "include": ["src", "package.json"], + "exclude": ["src/**/*.test.*"], } diff --git a/ironfish/tsconfig.json b/ironfish/tsconfig.json index 1e74446feb..26971efaa2 100644 --- a/ironfish/tsconfig.json +++ b/ironfish/tsconfig.json @@ -1,8 +1,8 @@ { "extends": "../config/tsconfig.base.json", "compilerOptions": { - "outDir": "build" + "noEmit": true, + "tsBuildInfoFile": "./build/tsconfig.tsbuildinfo" }, "include": ["src", "package.json"], - "exclude": ["src/**/*.test.*"], } From 2e6b3ad2ab24c96fa98e998a32ec6e47cb1672d3 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Tue, 1 Oct 2024 16:21:05 -0700 Subject: [PATCH 06/73] Store backup of ledger account on disk (#5438) * Store backup of ledger account on disk * Simplify code --------- Co-authored-by: Jason Spafford --- .../src/commands/wallet/multisig/dkg/create.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index 12377e2ead..147c0fb43f 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -14,6 +14,8 @@ import { RpcClient, } from '@ironfish/sdk' import { Flags } from '@oclif/core' +import fs from 'fs' +import path from 'path' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' import * as ui from '../../../../ui' @@ -519,6 +521,18 @@ export class DkgCreateCommand extends IronfishCommand { this.log( 'Use `ironfish wallet:multisig:ledger:restore` if you need to restore the keys to your Ledger.', ) + + const dataDir = this.sdk.fileSystem.resolve(this.sdk.dataDir) + const backupKeysPath = path.join(dataDir, `ironfish-ledger-${accountName}.txt`) + + if (fs.existsSync(backupKeysPath)) { + await ui.confirmOrQuit( + `Error when backing up your keys: \nThe file ${backupKeysPath} already exists. \nOverwrite?`, + ) + } + + await fs.promises.writeFile(backupKeysPath, encryptedKeys.toString('hex')) + this.log(`A copy of your encrypted keys have been saved at ${backupKeysPath}`) } async performRound3( From 62430e000660398dd327343c6f181bf1a9bd04dd Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 1 Oct 2024 17:59:17 -0700 Subject: [PATCH 07/73] Continously ask for name if duplicate (#5463) This fixes a bug where you only re-ask for a name if it's duplicated once. --- .../commands/wallet/multisig/dkg/create.ts | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index 147c0fb43f..c1152136fa 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -174,19 +174,22 @@ export class DkgCreateCommand extends IronfishCommand { } } - private async getAccountName(client: RpcClient, accountName?: string) { - let name: string - if (accountName) { - name = accountName - } else { - name = await ui.inputPrompt('Enter a name for the new multisig account', true) - } + private async getAccountName(client: RpcClient, name?: string) { + // eslint-disable-next-line no-constant-condition + while (true) { + if (!name) { + name = await ui.inputPrompt('Enter a name for the multisig account', true) + } - const accounts = (await client.wallet.getAccounts()).content.accounts + const accounts = (await client.wallet.getAccounts()).content.accounts + + if (accounts.find((a) => a === name)) { + this.log('An account with the same name already exists') + name = undefined + continue + } - if (accounts.find((a) => a === name)) { - this.log('An account with the same name already exists') - name = await ui.inputPrompt('Enter a new name for the account', true) + break } return name From 58a1b3ca93c0dd8ee6be51564ddf1a0808bd7a7c Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:23:01 -0700 Subject: [PATCH 08/73] adds multisig server to broker dkg messages (#5459) * adds multisig server to broker dkg messages copies stratum module from sdk into cli to serve as foundation for multisig network module replaces stratum mining messages with dkg messages: - 'dkg.status': returns the dkg status of the session including minSigners, identities, and all public packages - 'dkg.get_status': requests the status of the dkg session from the server - 'dkg.identity': sends an identity over the socket - 'dkg.round1': sends a round1 public package over the socket - 'dkg.round2': sends a round2 public package over the socket reuses single message for submitting an identity to the server and broadcasting it from the server. likewise for round1 public packages and round2 public packages adds a cli command, 'wallet:multisig:server', to start a server for a dkg session. requires maxSigners and minSigners on start to set the parameters for DKG updates 'wallet:multisig:dkg:create' with a '--server' flag to connect to a server. at each step, submits the participant's information to the server and then waits for server to broadcast packages from other participants. uses 'dkg.get_status' to get the status at the beginning of each step to account for the possibility of having missed broadcasts * adds sessions to multisig server (#5462) supports using a single server for multiple runs of dkg and/or signing updates 'wallet:multisig:dkg:create' to join an existing session with the 'sessionId' flag or create a new session if none is specified --- .../commands/wallet/multisig/dkg/create.ts | 285 ++++++++++--- .../src/commands/wallet/multisig/server.ts | 38 ++ ironfish-cli/src/utils/multisig/index.ts | 4 + .../multisig/network/adapters/adapter.ts | 29 ++ .../utils/multisig/network/adapters/index.ts | 7 + .../multisig/network/adapters/tcpAdapter.ts | 65 +++ .../multisig/network/adapters/tlsAdapter.ts | 30 ++ .../utils/multisig/network/clients/client.ts | 270 ++++++++++++ .../utils/multisig/network/clients/index.ts | 7 + .../multisig/network/clients/tcpClient.ts | 66 +++ .../multisig/network/clients/tlsClient.ts | 37 ++ .../src/utils/multisig/network/constants.ts | 9 + .../src/utils/multisig/network/errors.ts | 42 ++ .../src/utils/multisig/network/index.ts | 6 + .../src/utils/multisig/network/messages.ts | 110 +++++ .../src/utils/multisig/network/server.ts | 383 ++++++++++++++++++ .../utils/multisig/network/serverClient.ts | 38 ++ .../{multisig.ts => multisig/transaction.ts} | 0 18 files changed, 1361 insertions(+), 65 deletions(-) create mode 100644 ironfish-cli/src/commands/wallet/multisig/server.ts create mode 100644 ironfish-cli/src/utils/multisig/index.ts create mode 100644 ironfish-cli/src/utils/multisig/network/adapters/adapter.ts create mode 100644 ironfish-cli/src/utils/multisig/network/adapters/index.ts create mode 100644 ironfish-cli/src/utils/multisig/network/adapters/tcpAdapter.ts create mode 100644 ironfish-cli/src/utils/multisig/network/adapters/tlsAdapter.ts create mode 100644 ironfish-cli/src/utils/multisig/network/clients/client.ts create mode 100644 ironfish-cli/src/utils/multisig/network/clients/index.ts create mode 100644 ironfish-cli/src/utils/multisig/network/clients/tcpClient.ts create mode 100644 ironfish-cli/src/utils/multisig/network/clients/tlsClient.ts create mode 100644 ironfish-cli/src/utils/multisig/network/constants.ts create mode 100644 ironfish-cli/src/utils/multisig/network/errors.ts create mode 100644 ironfish-cli/src/utils/multisig/network/index.ts create mode 100644 ironfish-cli/src/utils/multisig/network/messages.ts create mode 100644 ironfish-cli/src/utils/multisig/network/server.ts create mode 100644 ironfish-cli/src/utils/multisig/network/serverClient.ts rename ironfish-cli/src/utils/{multisig.ts => multisig/transaction.ts} (100%) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index c1152136fa..34ac36da4a 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -11,15 +11,19 @@ import { AccountFormat, Assert, encodeAccountImport, + parseUrl, + PromiseUtils, RpcClient, } from '@ironfish/sdk' -import { Flags } from '@oclif/core' +import { Flags, ux } from '@oclif/core' +import dns from 'dns' import fs from 'fs' import path from 'path' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' import * as ui from '../../../../ui' import { LedgerDkg } from '../../../../utils/ledger' +import { MultisigTcpClient } from '../../../../utils/multisig/network' export class DkgCreateCommand extends IronfishCommand { static description = 'Interactive command to create a multisignature account using DKG' @@ -42,6 +46,13 @@ export class DkgCreateCommand extends IronfishCommand { description: "Block sequence to begin scanning from for the created account. Uses node's chain head by default", }), + server: Flags.string({ + description: "multisig server to connect to. formatted as ':'", + }), + sessionId: Flags.string({ + description: 'Unique ID for a multisig server session to join', + dependsOn: ['server'], + }), } async start(): Promise { @@ -72,6 +83,25 @@ export class DkgCreateCommand extends IronfishCommand { accountCreatedAt = statusResponse.content.blockchain.head.sequence } + let multisigClient: MultisigTcpClient | null = null + if (flags.server) { + const parsed = parseUrl(flags.server) + + Assert.isNotNull(parsed.hostname) + Assert.isNotNull(parsed.port) + + const resolved = await dns.promises.lookup(parsed.hostname) + const host = resolved.address + const port = parsed.port + + multisigClient = new MultisigTcpClient({ host, port, logger: this.logger }) + multisigClient.start() + + if (flags.sessionId) { + multisigClient.joinSession(flags.sessionId) + } + } + const { name: participantName, identity } = ledger ? await ui.retryStep( () => { @@ -85,9 +115,25 @@ export class DkgCreateCommand extends IronfishCommand { this.log(`Identity for ${participantName}: \n${identity} \n`) - const { round1, totalParticipants } = await ui.retryStep( + const { totalParticipants, minSigners } = await ui.retryStep( + async () => { + return this.getDkgConfig(multisigClient, !!ledger) + }, + this.logger, + true, + ) + + const { round1 } = await ui.retryStep( async () => { - return this.performRound1(client, participantName, identity, ledger) + return this.performRound1( + client, + multisigClient, + participantName, + identity, + totalParticipants, + minSigners, + ledger, + ) }, this.logger, true, @@ -101,11 +147,16 @@ export class DkgCreateCommand extends IronfishCommand { this.log(round1.publicPackage) this.log('\n============================================') - this.log('\nShare your Round 1 Public Package with other participants.') - const { round2: round2Result, round1PublicPackages } = await ui.retryStep( async () => { - return this.performRound2(client, participantName, round1, totalParticipants, ledger) + return this.performRound2( + client, + multisigClient, + participantName, + round1, + totalParticipants, + ledger, + ) }, this.logger, true, @@ -118,12 +169,12 @@ export class DkgCreateCommand extends IronfishCommand { this.log('\nRound 2 Public Package:') this.log(round2Result.publicPackage) this.log('\n============================================') - this.log('\nShare your Round 2 Public Package with other participants.') await ui.retryStep( async () => { return this.performRound3( client, + multisigClient, accountName, participantName, round2Result, @@ -138,6 +189,7 @@ export class DkgCreateCommand extends IronfishCommand { ) this.log('Multisig account created successfully using DKG!') + multisigClient?.stop() } private async getParticipant(client: RpcClient, participantName?: string) { @@ -251,6 +303,65 @@ export class DkgCreateCommand extends IronfishCommand { } } + async getDkgConfig( + multisigClient: MultisigTcpClient | null, + ledger: boolean, + ): Promise<{ totalParticipants: number; minSigners: number }> { + if (multisigClient?.sessionId) { + let totalParticipants = 0 + let minSigners = 0 + let waiting = true + multisigClient.onDkgStatus.on((message) => { + totalParticipants = message.maxSigners + minSigners = message.minSigners + waiting = false + }) + multisigClient.getDkgStatus() + + ux.action.start('Waiting for signer config from server') + while (waiting) { + await PromiseUtils.sleep(3000) + } + multisigClient.onDkgStatus.clear() + ux.action.stop() + + return { totalParticipants, minSigners } + } + + const totalParticipants = await ui.inputNumberPrompt( + this.logger, + 'Enter the total number of participants', + { required: true, integer: true }, + ) + + if (totalParticipants < 2) { + throw new Error('Total number of participants must be at least 2') + } + + if (ledger && totalParticipants > 4) { + throw new Error('DKG with Ledger supports a maximum of 4 participants') + } + + const minSigners = await ui.inputNumberPrompt( + this.logger, + 'Enter the number of minimum signers', + { required: true, integer: true }, + ) + + if (minSigners < 2 || minSigners > totalParticipants) { + throw new Error( + 'Minimum number of signers must be between 2 and the total number of participants', + ) + } + + if (multisigClient) { + multisigClient.startDkgSession(totalParticipants, minSigners) + this.log(`Started new DKG server session with ID ${multisigClient.sessionId}`) + } + + return { totalParticipants, minSigners } + } + async performRound1WithLedger( ledger: LedgerDkg, client: RpcClient, @@ -280,64 +391,59 @@ export class DkgCreateCommand extends IronfishCommand { async performRound1( client: RpcClient, + multisigClient: MultisigTcpClient | null, participantName: string, currentIdentity: string, + totalParticipants: number, + minSigners: number, ledger: LedgerDkg | undefined, ): Promise<{ round1: { secretPackage: string; publicPackage: string } - totalParticipants: number }> { this.log('\nCollecting Participant Info and Performing Round 1...') - const totalParticipants = await ui.inputNumberPrompt( - this.logger, - 'Enter the total number of participants', - { required: true, integer: true }, - ) - - if (totalParticipants < 2) { - throw new Error('Total number of participants must be at least 2') - } - - if (ledger && totalParticipants > 4) { - throw new Error('DKG with Ledger supports a maximum of 4 participants') - } - - this.log( - `\nEnter ${ - totalParticipants - 1 - } identities of all other participants (excluding yours) `, - ) - const identities = await ui.collectStrings('Participant Identity', totalParticipants - 1, { - additionalStrings: [currentIdentity], - errorOnDuplicate: true, - }) + let identities: string[] = [] + if (!multisigClient) { + this.log( + `\nEnter ${ + totalParticipants - 1 + } identities of all other participants (excluding yours) `, + ) + identities = await ui.collectStrings('Participant Identity', totalParticipants - 1, { + additionalStrings: [currentIdentity], + errorOnDuplicate: true, + }) + } else { + multisigClient.submitIdentity(currentIdentity) + + multisigClient.onDkgStatus.on((message) => { + identities = message.identities + }) + multisigClient.onIdentity.on((message) => { + if (!identities.includes(message.identity)) { + identities.push(message.identity) + } + }) - const minSigners = await ui.inputNumberPrompt( - this.logger, - 'Enter the number of minimum signers', - { required: true, integer: true }, - ) + ux.action.start('Waiting for other Identities from server') + while (identities.length < totalParticipants) { + multisigClient.getDkgStatus() + await PromiseUtils.sleep(3000) + } - if (minSigners < 2 || minSigners > totalParticipants) { - throw new Error( - 'Minimum number of signers must be between 2 and the total number of participants', - ) + multisigClient.onDkgStatus.clear() + multisigClient.onIdentity.clear() + ux.action.stop() } if (ledger) { - const result = await this.performRound1WithLedger( + return await this.performRound1WithLedger( ledger, client, participantName, identities, minSigners, ) - - return { - ...result, - totalParticipants, - } } this.log('\nPerforming DKG Round 1...') @@ -352,7 +458,6 @@ export class DkgCreateCommand extends IronfishCommand { secretPackage: response.content.round1SecretPackage, publicPackage: response.content.round1PublicPackage, }, - totalParticipants, } } @@ -380,6 +485,7 @@ export class DkgCreateCommand extends IronfishCommand { async performRound2( client: RpcClient, + multisigClient: MultisigTcpClient | null, participantName: string, round1Result: { secretPackage: string; publicPackage: string }, totalParticipants: number, @@ -388,16 +494,40 @@ export class DkgCreateCommand extends IronfishCommand { round2: { secretPackage: string; publicPackage: string } round1PublicPackages: string[] }> { - this.log(`\nEnter ${totalParticipants - 1} Round 1 Public Packages (excluding yours) `) + let round1PublicPackages: string[] = [] + if (!multisigClient) { + this.log('\nShare your Round 1 Public Package with other participants.') + this.log(`\nEnter ${totalParticipants - 1} Round 1 Public Packages (excluding yours) `) + + round1PublicPackages = await ui.collectStrings( + 'Round 1 Public Package', + totalParticipants - 1, + { + additionalStrings: [round1Result.publicPackage], + errorOnDuplicate: true, + }, + ) + } else { + multisigClient.submitRound1PublicPackage(round1Result.publicPackage) + multisigClient.onDkgStatus.on((message) => { + round1PublicPackages = message.round1PublicPackages + }) + multisigClient.onRound1PublicPackage.on((message) => { + if (!round1PublicPackages.includes(message.package)) { + round1PublicPackages.push(message.package) + } + }) - const round1PublicPackages = await ui.collectStrings( - 'Round 1 Public Package', - totalParticipants - 1, - { - additionalStrings: [round1Result.publicPackage], - errorOnDuplicate: true, - }, - ) + ux.action.start('Waiting for other Round 1 Public Packages from server') + while (round1PublicPackages.length < totalParticipants) { + multisigClient.getDkgStatus() + await PromiseUtils.sleep(3000) + } + + multisigClient.onDkgStatus.clear() + multisigClient.onRound1PublicPackage.clear() + ux.action.stop() + } this.log('\nPerforming DKG Round 2...') @@ -540,6 +670,7 @@ export class DkgCreateCommand extends IronfishCommand { async performRound3( client: RpcClient, + multisigClient: MultisigTcpClient | null, accountName: string, participantName: string, round2Result: { secretPackage: string; publicPackage: string }, @@ -548,16 +679,40 @@ export class DkgCreateCommand extends IronfishCommand { ledger: LedgerDkg | undefined, accountCreatedAt?: number, ): Promise { - this.log(`\nEnter ${totalParticipants - 1} Round 2 Public Packages (excluding yours) `) + let round2PublicPackages: string[] = [] + if (!multisigClient) { + this.log('\nShare your Round 2 Public Package with other participants.') + this.log(`\nEnter ${totalParticipants - 1} Round 2 Public Packages (excluding yours) `) + + round2PublicPackages = await ui.collectStrings( + 'Round 2 Public Package', + totalParticipants - 1, + { + additionalStrings: [round2Result.publicPackage], + errorOnDuplicate: true, + }, + ) + } else { + multisigClient.submitRound2PublicPackage(round2Result.publicPackage) + multisigClient.onDkgStatus.on((message) => { + round2PublicPackages = message.round2PublicPackages + }) + multisigClient.onRound2PublicPackage.on((message) => { + if (!round2PublicPackages.includes(message.package)) { + round2PublicPackages.push(message.package) + } + }) - const round2PublicPackages = await ui.collectStrings( - 'Round 2 Public Package', - totalParticipants - 1, - { - additionalStrings: [round2Result.publicPackage], - errorOnDuplicate: true, - }, - ) + ux.action.start('Waiting for other Round 2 Public Packages from server') + while (round2PublicPackages.length < totalParticipants) { + multisigClient.getDkgStatus() + await PromiseUtils.sleep(3000) + } + + multisigClient.onDkgStatus.clear() + multisigClient.onRound2PublicPackage.clear() + ux.action.stop() + } if (ledger) { await this.performRound3WithLedger( diff --git a/ironfish-cli/src/commands/wallet/multisig/server.ts b/ironfish-cli/src/commands/wallet/multisig/server.ts new file mode 100644 index 0000000000..5f19a8ff43 --- /dev/null +++ b/ironfish-cli/src/commands/wallet/multisig/server.ts @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { Flags } from '@oclif/core' +import { IronfishCommand } from '../../../command' +import { MultisigServer } from '../../../utils/multisig/network' +import { MultisigTcpAdapter } from '../../../utils/multisig/network/adapters' + +export class MultisigServerCommand extends IronfishCommand { + static description = 'start a server to broker messages for a multisig session' + + static flags = { + host: Flags.string({ + description: 'host address for the multisig server', + default: '::', + }), + port: Flags.integer({ + description: 'port for the multisig server', + default: 9035, + }), + } + + async start(): Promise { + const { flags } = await this.parse(MultisigServerCommand) + + const server = new MultisigServer({ logger: this.logger }) + + const adapter = new MultisigTcpAdapter({ + logger: this.logger, + host: flags.host, + port: flags.port, + }) + + server.mount(adapter) + await server.start() + } +} diff --git a/ironfish-cli/src/utils/multisig/index.ts b/ironfish-cli/src/utils/multisig/index.ts new file mode 100644 index 0000000000..1e6dfb1fea --- /dev/null +++ b/ironfish-cli/src/utils/multisig/index.ts @@ -0,0 +1,4 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +export * from './transaction' diff --git a/ironfish-cli/src/utils/multisig/network/adapters/adapter.ts b/ironfish-cli/src/utils/multisig/network/adapters/adapter.ts new file mode 100644 index 0000000000..9a8500dee6 --- /dev/null +++ b/ironfish-cli/src/utils/multisig/network/adapters/adapter.ts @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { MultisigServer } from '../server' + +/** + * An adapter represents a network transport that accepts connections from + * clients and routes them into the server. + */ +export interface IStratumAdapter { + /** + * Called when the adapter is added to a MultisigServer. + */ + attach(server: MultisigServer): void + + /** + * Called when the adapter should start serving requests to the server + * This is when an adapter would normally listen on a port for data and + * create {@link Request } for the routing layer. + * + * For example, when an + * HTTP server starts listening, or an IPC layer opens an IPC socket. + */ + start(): Promise + + /** Called when the adapter should stop serving requests to the server. */ + stop(): Promise +} diff --git a/ironfish-cli/src/utils/multisig/network/adapters/index.ts b/ironfish-cli/src/utils/multisig/network/adapters/index.ts new file mode 100644 index 0000000000..436cbdef3f --- /dev/null +++ b/ironfish-cli/src/utils/multisig/network/adapters/index.ts @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +export * from './adapter' +export * from './tcpAdapter' +export * from './tlsAdapter' diff --git a/ironfish-cli/src/utils/multisig/network/adapters/tcpAdapter.ts b/ironfish-cli/src/utils/multisig/network/adapters/tcpAdapter.ts new file mode 100644 index 0000000000..c2e787f9de --- /dev/null +++ b/ironfish-cli/src/utils/multisig/network/adapters/tcpAdapter.ts @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { Logger } from '@ironfish/sdk' +import net from 'net' +import { MultisigServer } from '../server' +import { IStratumAdapter } from './adapter' + +export class MultisigTcpAdapter implements IStratumAdapter { + server: net.Server | null = null + multisigServer: MultisigServer | null = null + readonly logger: Logger + + readonly host: string + readonly port: number + + started = false + + constructor(options: { logger: Logger; host: string; port: number }) { + this.logger = options.logger + this.host = options.host + this.port = options.port + } + + protected createServer(): net.Server { + this.logger.info(`Hosting Multisig Server via TCP on ${this.host}:${this.port}`) + + return net.createServer((socket) => this.multisigServer?.onConnection(socket)) + } + + start(): Promise { + if (this.started) { + return Promise.resolve() + } + + this.started = true + + return new Promise((resolve, reject) => { + try { + this.server = this.createServer() + this.server.listen(this.port, this.host, () => { + resolve() + }) + } catch (e) { + reject(e) + } + }) + } + + stop(): Promise { + if (!this.started) { + return Promise.resolve() + } + + return new Promise((resolve, reject) => { + this.server?.close((e) => { + return e ? reject(e) : resolve() + }) + }) + } + + attach(server: MultisigServer): void { + this.multisigServer = server + } +} diff --git a/ironfish-cli/src/utils/multisig/network/adapters/tlsAdapter.ts b/ironfish-cli/src/utils/multisig/network/adapters/tlsAdapter.ts new file mode 100644 index 0000000000..5669b73d13 --- /dev/null +++ b/ironfish-cli/src/utils/multisig/network/adapters/tlsAdapter.ts @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { Logger } from '@ironfish/sdk' +import net from 'net' +import tls from 'tls' +import { MultisigTcpAdapter } from './tcpAdapter' + +export class MultisigTlsAdapter extends MultisigTcpAdapter { + readonly tlsOptions: tls.TlsOptions + + constructor(options: { + logger: Logger + host: string + port: number + tlsOptions: tls.TlsOptions + }) { + super(options) + + this.tlsOptions = options.tlsOptions + } + + protected createServer(): net.Server { + this.logger.info(`Hosting Multisig Server via TLS on ${this.host}:${this.port}`) + + return tls.createServer(this.tlsOptions, (socket) => + this.multisigServer?.onConnection(socket), + ) + } +} diff --git a/ironfish-cli/src/utils/multisig/network/clients/client.ts b/ironfish-cli/src/utils/multisig/network/clients/client.ts new file mode 100644 index 0000000000..2155d02b7a --- /dev/null +++ b/ironfish-cli/src/utils/multisig/network/clients/client.ts @@ -0,0 +1,270 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { + ErrorUtils, + Event, + Logger, + MessageBuffer, + SetTimeoutToken, + YupUtils, +} from '@ironfish/sdk' +import { v4 as uuid } from 'uuid' +import { ServerMessageMalformedError } from '../errors' +import { + DkgGetStatusMessage, + DkgStartSessionMessage, + DkgStatusMessage, + DkgStatusSchema, + IdentityMessage, + IdentitySchema, + JoinSessionMessage, + Round1PublicPackageMessage, + Round1PublicPackageSchema, + Round2PublicPackageMessage, + Round2PublicPackageSchema, + StratumMessage, + StratumMessageSchema, + StratumMessageWithError, + StratumMessageWithErrorSchema, +} from '../messages' + +export abstract class MultisigClient { + readonly logger: Logger + readonly version: number + + private started: boolean + private isClosing = false + private connected: boolean + private connectWarned: boolean + private connectTimeout: SetTimeoutToken | null + private nextMessageId: number + private readonly messageBuffer = new MessageBuffer('\n') + + private disconnectUntil: number | null = null + + readonly onConnected = new Event<[]>() + readonly onIdentity = new Event<[IdentityMessage]>() + readonly onRound1PublicPackage = new Event<[Round1PublicPackageMessage]>() + readonly onRound2PublicPackage = new Event<[Round2PublicPackageMessage]>() + readonly onDkgStatus = new Event<[DkgStatusMessage]>() + readonly onStratumError = new Event<[StratumMessageWithError]>() + + sessionId: string | null = null + + constructor(options: { logger: Logger }) { + this.logger = options.logger + this.version = 3 + + this.started = false + this.nextMessageId = 0 + this.connected = false + this.connectWarned = false + this.connectTimeout = null + } + + protected abstract connect(): Promise + protected abstract writeData(data: string): void + protected abstract close(): Promise + + start(): void { + if (this.started) { + return + } + + this.started = true + this.logger.debug('Connecting to server...') + void this.startConnecting() + } + + private async startConnecting(): Promise { + if (this.isClosing) { + return + } + + if (this.disconnectUntil && this.disconnectUntil > Date.now()) { + this.connectTimeout = setTimeout(() => void this.startConnecting(), 60 * 1000) + return + } + + const connected = await this.connect() + .then(() => true) + .catch(() => false) + + if (!this.started) { + return + } + + if (!connected) { + if (!this.connectWarned) { + this.logger.warn(`Failed to connect to server, retrying...`) + this.connectWarned = true + } + + this.connectTimeout = setTimeout(() => void this.startConnecting(), 5000) + return + } + + this.connectWarned = false + this.onConnect() + this.onConnected.emit() + } + + stop(): void { + this.isClosing = true + void this.close() + + if (this.connectTimeout) { + clearTimeout(this.connectTimeout) + } + } + + isConnected(): boolean { + return this.connected + } + + joinSession(sessionId: string): void { + this.sessionId = sessionId + this.send('join_session', {}) + } + + startDkgSession(maxSigners: number, minSigners: number): void { + this.sessionId = uuid() + this.send('dkg.start_session', { maxSigners, minSigners }) + } + + submitIdentity(identity: string): void { + this.send('identity', { identity }) + } + + submitRound1PublicPackage(round1PublicPackage: string): void { + this.send('dkg.round1', { package: round1PublicPackage }) + } + + submitRound2PublicPackage(round2PublicPackage: string): void { + this.send('dkg.round2', { package: round2PublicPackage }) + } + + getDkgStatus(): void { + this.send('dkg.get_status', {}) + } + + private send(method: 'join_session', body: JoinSessionMessage): void + private send(method: 'dkg.start_session', body: DkgStartSessionMessage): void + private send(method: 'identity', body: IdentityMessage): void + private send(method: 'dkg.round1', body: Round1PublicPackageMessage): void + private send(method: 'dkg.round2', body: Round2PublicPackageMessage): void + private send(method: 'dkg.get_status', body: DkgGetStatusMessage): void + private send(method: string, body?: unknown): void { + if (!this.sessionId) { + throw new Error('Client must join a session before sending messages') + } + + if (!this.connected) { + return + } + + const message: StratumMessage = { + id: this.nextMessageId++, + method, + sessionId: this.sessionId, + body, + } + + this.writeData(JSON.stringify(message) + '\n') + } + + protected onConnect(): void { + this.connected = true + + this.logger.debug('Successfully connected to server') + } + + protected onDisconnect = (): void => { + this.connected = false + this.messageBuffer.clear() + + if (!this.isClosing) { + this.logger.warn('Disconnected from server unexpectedly. Reconnecting.') + this.connectTimeout = setTimeout(() => void this.startConnecting(), 5000) + } + } + + protected onError = (error: unknown): void => { + this.logger.error(`Error ${ErrorUtils.renderError(error)}`) + } + + protected async onData(data: Buffer): Promise { + this.messageBuffer.write(data) + + for (const message of this.messageBuffer.readMessages()) { + const payload: unknown = JSON.parse(message) + + const header = await YupUtils.tryValidate(StratumMessageSchema, payload) + + if (header.error) { + // Try the error message instead. + const headerWithError = await YupUtils.tryValidate( + StratumMessageWithErrorSchema, + payload, + ) + if (headerWithError.error) { + throw new ServerMessageMalformedError(header.error) + } + this.logger.debug( + `Server sent error ${headerWithError.result.error.message} for id ${headerWithError.result.error.id}`, + ) + this.onStratumError.emit(headerWithError.result) + return + } + + this.logger.debug(`Server sent ${header.result.method} message`) + + switch (header.result.method) { + case 'identity': { + const body = await YupUtils.tryValidate(IdentitySchema, header.result.body) + + if (body.error) { + throw new ServerMessageMalformedError(body.error, header.result.method) + } + + this.onIdentity.emit(body.result) + break + } + case 'dkg.round1': { + const body = await YupUtils.tryValidate(Round1PublicPackageSchema, header.result.body) + + if (body.error) { + throw new ServerMessageMalformedError(body.error, header.result.method) + } + + this.onRound1PublicPackage.emit(body.result) + break + } + case 'dkg.round2': { + const body = await YupUtils.tryValidate(Round2PublicPackageSchema, header.result.body) + + if (body.error) { + throw new ServerMessageMalformedError(body.error, header.result.method) + } + + this.onRound2PublicPackage.emit(body.result) + break + } + case 'dkg.status': { + const body = await YupUtils.tryValidate(DkgStatusSchema, header.result.body) + + if (body.error) { + throw new ServerMessageMalformedError(body.error, header.result.method) + } + + this.onDkgStatus.emit(body.result) + break + } + + default: + throw new ServerMessageMalformedError(`Invalid message ${header.result.method}`) + } + } + } +} diff --git a/ironfish-cli/src/utils/multisig/network/clients/index.ts b/ironfish-cli/src/utils/multisig/network/clients/index.ts new file mode 100644 index 0000000000..5a5d770c3a --- /dev/null +++ b/ironfish-cli/src/utils/multisig/network/clients/index.ts @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +export * from './client' +export * from './tcpClient' +export * from './tlsClient' diff --git a/ironfish-cli/src/utils/multisig/network/clients/tcpClient.ts b/ironfish-cli/src/utils/multisig/network/clients/tcpClient.ts new file mode 100644 index 0000000000..4465d3ba14 --- /dev/null +++ b/ironfish-cli/src/utils/multisig/network/clients/tcpClient.ts @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { Logger } from '@ironfish/sdk' +import net from 'net' +import { MultisigClient } from './client' + +export class MultisigTcpClient extends MultisigClient { + readonly host: string + readonly port: number + + client: net.Socket | null = null + + constructor(options: { host: string; port: number; logger: Logger }) { + super({ logger: options.logger }) + this.host = options.host + this.port = options.port + } + + protected onSocketDisconnect = (): void => { + this.client?.off('error', this.onError) + this.client?.off('close', this.onSocketDisconnect) + this.client?.off('data', this.onSocketData) + this.onDisconnect() + } + + protected onSocketData = (data: Buffer): void => { + this.onData(data).catch((e) => this.onError(e)) + } + + protected connect(): Promise { + return new Promise((resolve, reject): void => { + const onConnect = () => { + client.off('connect', onConnect) + client.off('error', onError) + + client.on('error', this.onError) + client.on('close', this.onSocketDisconnect) + + resolve() + } + + const onError = (error: unknown) => { + client.off('connect', onConnect) + client.off('error', onError) + reject(error) + } + + const client = new net.Socket() + client.on('error', onError) + client.on('connect', onConnect) + client.on('data', this.onSocketData) + client.connect({ host: this.host, port: this.port }) + this.client = client + }) + } + + protected writeData(data: string): void { + this.client?.write(data) + } + + protected close(): Promise { + this.client?.destroy() + return Promise.resolve() + } +} diff --git a/ironfish-cli/src/utils/multisig/network/clients/tlsClient.ts b/ironfish-cli/src/utils/multisig/network/clients/tlsClient.ts new file mode 100644 index 0000000000..280a3b766b --- /dev/null +++ b/ironfish-cli/src/utils/multisig/network/clients/tlsClient.ts @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import tls from 'tls' +import { MultisigTcpClient } from './tcpClient' + +export class MultisigTlsClient extends MultisigTcpClient { + protected connect(): Promise { + return new Promise((resolve, reject): void => { + const onConnect = () => { + client.off('secureConnect', onConnect) + client.off('error', onError) + + client.on('error', this.onError) + client.on('close', this.onSocketDisconnect) + + resolve() + } + + const onError = (error: unknown) => { + client.off('secureConnect', onConnect) + client.off('error', onError) + reject(error) + } + + const client = tls.connect({ + host: this.host, + port: this.port, + rejectUnauthorized: false, + }) + client.on('error', onError) + client.on('secureConnect', onConnect) + client.on('data', this.onSocketData) + this.client = client + }) + } +} diff --git a/ironfish-cli/src/utils/multisig/network/constants.ts b/ironfish-cli/src/utils/multisig/network/constants.ts new file mode 100644 index 0000000000..437f002a98 --- /dev/null +++ b/ironfish-cli/src/utils/multisig/network/constants.ts @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +export enum DisconnectReason { + BAD_VERSION = 'bad_version', + BANNED = 'banned', + UNKNOWN = 'unknown', +} diff --git a/ironfish-cli/src/utils/multisig/network/errors.ts b/ironfish-cli/src/utils/multisig/network/errors.ts new file mode 100644 index 0000000000..5f0b8131e8 --- /dev/null +++ b/ironfish-cli/src/utils/multisig/network/errors.ts @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import * as yup from 'yup' +import { MultisigServerClient } from './serverClient' + +export class MessageMalformedError extends Error { + name = this.constructor.name + + constructor(sender: string, error: yup.ValidationError | string, method?: string) { + super() + + if (typeof error === 'string') { + this.message = error + } else { + this.message = `${sender} sent malformed request` + if (method) { + this.message += ` (${method})` + } + this.message += `: ${error.message}` + } + } +} + +export class ClientMessageMalformedError extends MessageMalformedError { + client: MultisigServerClient + + constructor( + client: MultisigServerClient, + error: yup.ValidationError | string, + method?: string, + ) { + super(`Client ${client.id}`, error, method) + this.client = client + } +} + +export class ServerMessageMalformedError extends MessageMalformedError { + constructor(error: yup.ValidationError | string, method?: string) { + super('Server', error, method) + } +} diff --git a/ironfish-cli/src/utils/multisig/network/index.ts b/ironfish-cli/src/utils/multisig/network/index.ts new file mode 100644 index 0000000000..59dd82a39b --- /dev/null +++ b/ironfish-cli/src/utils/multisig/network/index.ts @@ -0,0 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +export * from './clients' +export { MultisigServer } from './server' diff --git a/ironfish-cli/src/utils/multisig/network/messages.ts b/ironfish-cli/src/utils/multisig/network/messages.ts new file mode 100644 index 0000000000..f8f921b218 --- /dev/null +++ b/ironfish-cli/src/utils/multisig/network/messages.ts @@ -0,0 +1,110 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import * as yup from 'yup' + +export type StratumMessage = { + id: number + method: string + sessionId: string + body?: unknown +} + +export interface StratumMessageWithError + extends Omit { + error: { + id: number + message: string + } +} + +export type DkgStartSessionMessage = { + minSigners: number + maxSigners: number +} + +export type JoinSessionMessage = object | undefined + +export type IdentityMessage = { + identity: string +} + +export type Round1PublicPackageMessage = { + package: string +} + +export type Round2PublicPackageMessage = { + package: string +} + +export type DkgGetStatusMessage = object | undefined + +export type DkgStatusMessage = { + minSigners: number + maxSigners: number + identities: string[] + round1PublicPackages: string[] + round2PublicPackages: string[] +} + +export const StratumMessageSchema: yup.ObjectSchema = yup + .object({ + id: yup.number().required(), + method: yup.string().required(), + sessionId: yup.string().required(), + body: yup.mixed().notRequired(), + }) + .required() + +export const StratumMessageWithErrorSchema: yup.ObjectSchema = yup + .object({ + id: yup.number().required(), + error: yup + .object({ + id: yup.number().required(), + message: yup.string().required(), + }) + .required(), + }) + .required() + +export const DkgStartSessionSchema: yup.ObjectSchema = yup + .object({ + minSigners: yup.number().defined(), + maxSigners: yup.number().defined(), + }) + .defined() + +export const JoinSessionSchema: yup.ObjectSchema = yup + .object({}) + .notRequired() + .default(undefined) + +export const IdentitySchema: yup.ObjectSchema = yup + .object({ + identity: yup.string().defined(), + }) + .defined() + +export const Round1PublicPackageSchema: yup.ObjectSchema = yup + .object({ package: yup.string().defined() }) + .defined() + +export const Round2PublicPackageSchema: yup.ObjectSchema = yup + .object({ package: yup.string().defined() }) + .defined() + +export const DkgGetStatusSchema: yup.ObjectSchema = yup + .object({}) + .notRequired() + .default(undefined) + +export const DkgStatusSchema: yup.ObjectSchema = yup + .object({ + minSigners: yup.number().defined(), + maxSigners: yup.number().defined(), + identities: yup.array(yup.string().defined()).defined(), + round1PublicPackages: yup.array(yup.string().defined()).defined(), + round2PublicPackages: yup.array(yup.string().defined()).defined(), + }) + .defined() diff --git a/ironfish-cli/src/utils/multisig/network/server.ts b/ironfish-cli/src/utils/multisig/network/server.ts new file mode 100644 index 0000000000..d8f7b8a5be --- /dev/null +++ b/ironfish-cli/src/utils/multisig/network/server.ts @@ -0,0 +1,383 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { ErrorUtils, Logger, YupUtils } from '@ironfish/sdk' +import net from 'net' +import { IStratumAdapter } from './adapters' +import { ClientMessageMalformedError } from './errors' +import { + DkgGetStatusSchema, + DkgStartSessionSchema, + DkgStatusMessage, + IdentityMessage, + IdentitySchema, + Round1PublicPackageMessage, + Round1PublicPackageSchema, + Round2PublicPackageMessage, + Round2PublicPackageSchema, + StratumMessage, + StratumMessageSchema, + StratumMessageWithError, +} from './messages' +import { MultisigServerClient } from './serverClient' + +export type DkgStatus = { + minSigners: number + maxSigners: number + identities: string[] + round1PublicPackages: string[] + round2PublicPackages: string[] +} + +export class MultisigServer { + readonly logger: Logger + readonly adapters: IStratumAdapter[] = [] + + clients: Map + nextClientId: number + nextMessageId: number + + sessions: Map = new Map() + + private _isRunning = false + private _startPromise: Promise | null = null + + constructor(options: { logger: Logger; banning?: boolean }) { + this.logger = options.logger + + this.clients = new Map() + this.nextClientId = 1 + this.nextMessageId = 1 + } + + get isRunning(): boolean { + return this._isRunning + } + + /** Starts the Stratum server and tells any attached adapters to start serving requests */ + async start(): Promise { + if (this._isRunning) { + return + } + + this._startPromise = Promise.all(this.adapters.map((a) => a.start())) + this._isRunning = true + await this._startPromise + } + + /** Stops the Stratum server and tells any attached adapters to stop serving requests */ + async stop(): Promise { + if (!this._isRunning) { + return + } + + if (this._startPromise) { + await this._startPromise + } + + await Promise.all(this.adapters.map((a) => a.stop())) + this._isRunning = false + } + + /** Adds an adapter to the Stratum server and starts it if the server has already been started */ + mount(adapter: IStratumAdapter): void { + this.adapters.push(adapter) + adapter.attach(this) + + if (this._isRunning) { + let promise: Promise = adapter.start() + + if (this._startPromise) { + // Attach this promise to the start promise chain + // in case we call stop while were still starting up + promise = Promise.all([this._startPromise, promise]) + } + + this._startPromise = promise + } + } + + onConnection(socket: net.Socket): void { + const client = MultisigServerClient.accept(socket, this.nextClientId++) + + socket.on('data', (data: Buffer) => { + this.onData(client, data).catch((e) => this.onError(client, e)) + }) + + socket.on('close', () => this.onDisconnect(client)) + socket.on('error', (e) => this.onError(client, e)) + + this.logger.debug(`Client ${client.id} connected: ${client.remoteAddress}`) + this.clients.set(client.id, client) + } + + private onDisconnect(client: MultisigServerClient): void { + this.logger.debug(`Client ${client.id} disconnected (${this.clients.size - 1} total)`) + + this.clients.delete(client.id) + client.close() + client.socket.removeAllListeners('close') + client.socket.removeAllListeners('error') + } + + private async onData(client: MultisigServerClient, data: Buffer): Promise { + client.messageBuffer.write(data) + + for (const split of client.messageBuffer.readMessages()) { + const payload: unknown = JSON.parse(split) + const { error: parseError, result: message } = await YupUtils.tryValidate( + StratumMessageSchema, + payload, + ) + + if (parseError) { + this.sendStratumError(client, 0, `Error parsing message`) + return + } + + this.logger.debug(`Client ${client.id} sent ${message.method} message`) + + if (message.method === 'dkg.start_session') { + await this.handleDkgStartSessionMessage(client, message) + return + } else if (message.method === 'join_session') { + this.handleJoinSessionMessage(client, message) + return + } else if (message.method === 'identity') { + await this.handleIdentityMessage(client, message) + return + } else if (message.method === 'dkg.round1') { + await this.handleRound1PublicPackageMessage(client, message) + return + } else if (message.method === 'dkg.round2') { + await this.handleRound2PublicPackageMessage(client, message) + return + } else if (message.method === 'dkg.get_status') { + await this.handleDkgGetStatusMessage(client, message) + return + } else { + throw new ClientMessageMalformedError(client, `Invalid message ${message.method}`) + } + } + } + + private onError(client: MultisigServerClient, error: unknown): void { + this.logger.debug( + `Error during handling of data from client ${client.id}: ${ErrorUtils.renderError( + error, + true, + )}`, + ) + + client.socket.removeAllListeners() + client.close() + + this.clients.delete(client.id) + } + + private broadcast(method: 'identity', sessionId: string, body: IdentityMessage): void + private broadcast( + method: 'dkg.round1', + sessionId: string, + body: Round1PublicPackageMessage, + ): void + private broadcast( + method: 'dkg.round2', + sessionId: string, + body: Round2PublicPackageMessage, + ): void + private broadcast(method: string, sessionId: string, body?: unknown): void { + const message: StratumMessage = { + id: this.nextMessageId++, + method, + sessionId, + body, + } + + const serialized = JSON.stringify(message) + '\n' + + this.logger.debug('broadcasting to clients', { + method, + sessionId, + id: message.id, + numClients: this.clients.size, + messageLength: serialized.length, + }) + + let broadcasted = 0 + + for (const client of this.clients.values()) { + if (client.sessionId !== sessionId) { + continue + } + + if (!client.connected) { + continue + } + + client.socket.write(serialized) + broadcasted++ + } + + this.logger.debug('completed broadcast to clients', { + method, + sessionId, + id: message.id, + numClients: broadcasted, + messageLength: serialized.length, + }) + } + + send( + socket: net.Socket, + method: 'dkg.status', + sessionId: string, + body: DkgStatusMessage, + ): void + send(socket: net.Socket, method: string, sessionId: string, body?: unknown): void { + const message: StratumMessage = { + id: this.nextMessageId++, + method, + sessionId, + body, + } + + const serialized = JSON.stringify(message) + '\n' + socket.write(serialized) + } + + sendStratumError(client: MultisigServerClient, id: number, message: string): void { + const msg: StratumMessageWithError = { + id: this.nextMessageId++, + error: { + id: id, + message: message, + }, + } + const serialized = JSON.stringify(msg) + '\n' + client.socket.write(serialized) + } + + async handleDkgStartSessionMessage(client: MultisigServerClient, message: StratumMessage) { + const body = await YupUtils.tryValidate(DkgStartSessionSchema, message.body) + + if (body.error) { + return + } + + const sessionId = message.sessionId + + if (this.sessions.has(sessionId)) { + this.sendStratumError(client, message.id, `Duplicate sessionId: ${sessionId}`) + return + } + + this.sessions.set(sessionId, { + maxSigners: body.result.maxSigners, + minSigners: body.result.minSigners, + identities: [], + round1PublicPackages: [], + round2PublicPackages: [], + }) + + this.logger.debug(`Client ${client.id} started session ${message.sessionId}`) + + client.sessionId = message.sessionId + } + + handleJoinSessionMessage(client: MultisigServerClient, message: StratumMessage) { + if (!this.sessions.has(message.sessionId)) { + this.sendStratumError(client, message.id, `Session not found: ${message.sessionId}`) + return + } + + this.logger.debug(`Client ${client.id} joined session ${message.sessionId}`) + + client.sessionId = message.sessionId + } + + async handleIdentityMessage(client: MultisigServerClient, message: StratumMessage) { + const body = await YupUtils.tryValidate(IdentitySchema, message.body) + + if (body.error) { + return + } + + const session = this.sessions.get(message.sessionId) + if (!session) { + this.sendStratumError(client, message.id, `Session not found: ${message.sessionId}`) + return + } + + const identity = body.result.identity + if (!session.identities.includes(identity)) { + session.identities.push(identity) + this.sessions.set(message.sessionId, session) + this.broadcast('identity', message.sessionId, { identity }) + } + } + + async handleRound1PublicPackageMessage( + client: MultisigServerClient, + message: StratumMessage, + ) { + const body = await YupUtils.tryValidate(Round1PublicPackageSchema, message.body) + + if (body.error) { + return + } + + const session = this.sessions.get(message.sessionId) + if (!session) { + this.sendStratumError(client, message.id, `Session not found: ${message.sessionId}`) + return + } + + const round1PublicPackage = body.result.package + if (!session.round1PublicPackages.includes(round1PublicPackage)) { + session.round1PublicPackages.push(round1PublicPackage) + this.sessions.set(message.sessionId, session) + this.broadcast('dkg.round1', message.sessionId, { package: round1PublicPackage }) + } + } + + async handleRound2PublicPackageMessage( + client: MultisigServerClient, + message: StratumMessage, + ) { + const body = await YupUtils.tryValidate(Round2PublicPackageSchema, message.body) + + if (body.error) { + return + } + + const session = this.sessions.get(message.sessionId) + if (!session) { + this.sendStratumError(client, message.id, `Session not found: ${message.sessionId}`) + return + } + + const round2PublicPackage = body.result.package + if (!session.round2PublicPackages.includes(round2PublicPackage)) { + session.round2PublicPackages.push(round2PublicPackage) + this.sessions.set(message.sessionId, session) + this.broadcast('dkg.round2', message.sessionId, { package: round2PublicPackage }) + } + } + + async handleDkgGetStatusMessage(client: MultisigServerClient, message: StratumMessage) { + const body = await YupUtils.tryValidate(DkgGetStatusSchema, message.body) + + if (body.error) { + return + } + + const session = this.sessions.get(message.sessionId) + if (!session) { + this.sendStratumError(client, message.id, `Session not found: ${message.sessionId}`) + return + } + + this.send(client.socket, 'dkg.status', message.sessionId, session) + } +} diff --git a/ironfish-cli/src/utils/multisig/network/serverClient.ts b/ironfish-cli/src/utils/multisig/network/serverClient.ts new file mode 100644 index 0000000000..886f71f284 --- /dev/null +++ b/ironfish-cli/src/utils/multisig/network/serverClient.ts @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { Assert, MessageBuffer } from '@ironfish/sdk' +import net from 'net' + +export class MultisigServerClient { + id: number + socket: net.Socket + connected: boolean + remoteAddress: string + messageBuffer: MessageBuffer + sessionId: string | null = null + + private constructor(options: { socket: net.Socket; id: number }) { + this.id = options.id + this.socket = options.socket + this.connected = true + this.messageBuffer = new MessageBuffer('\n') + + Assert.isNotUndefined(this.socket.remoteAddress) + this.remoteAddress = this.socket.remoteAddress + } + + static accept(socket: net.Socket, id: number): MultisigServerClient { + return new MultisigServerClient({ socket, id }) + } + + close(error?: Error): void { + if (!this.connected) { + return + } + + this.messageBuffer.clear() + this.connected = false + this.socket.destroy(error) + } +} diff --git a/ironfish-cli/src/utils/multisig.ts b/ironfish-cli/src/utils/multisig/transaction.ts similarity index 100% rename from ironfish-cli/src/utils/multisig.ts rename to ironfish-cli/src/utils/multisig/transaction.ts From 71342890b0625c637a34ce9fe2f6c82c62ecfdfe Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Wed, 2 Oct 2024 13:28:53 -0700 Subject: [PATCH 09/73] Update zondax ironfish to 0.5.1 (#5449) * Upgrades Zondax JS to 5.1.0 We previously had to have two versions of this SDK available because the newer one did not work with single signer. Zondax fixed the bugs and now we don't need to manage two separate dependencies now. Created a base ledger class for the common functionality. * changing ledger class name to LedgerSingleSigner * adding new line back to package json' --- ironfish-cli/package.json | 4 +- ironfish-cli/src/commands/wallet/import.ts | 4 +- .../src/commands/wallet/transactions/sign.ts | 4 +- ironfish-cli/src/utils/ledger.ts | 186 ++++-------------- yarn.lock | 59 ++---- 5 files changed, 65 insertions(+), 192 deletions(-) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index 84ff196ead..707c10b567 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -69,9 +69,7 @@ "@oclif/plugin-warn-if-update-available": "3.1.8", "@types/keccak": "3.0.4", "@types/tar": "6.1.1", - "@zondax/ledger-ironfish": "0.1.2", - "@zondax/ledger-ironfish-dkg": "npm:@zondax/ledger-ironfish@0.4.0", - "@zondax/ledger-js": "^1.0.1", + "@zondax/ledger-ironfish": "0.5.1", "axios": "1.7.2", "bech32": "2.0.0", "blessed": "0.1.81", diff --git a/ironfish-cli/src/commands/wallet/import.ts b/ironfish-cli/src/commands/wallet/import.ts index 7c8e54badf..60c7c3cf5e 100644 --- a/ironfish-cli/src/commands/wallet/import.ts +++ b/ironfish-cli/src/commands/wallet/import.ts @@ -8,7 +8,7 @@ import { RemoteFlags } from '../../flags' import { checkWalletUnlocked, inputPrompt } from '../../ui' import { importFile, importPipe, longPrompt } from '../../ui/longPrompt' import { importAccount } from '../../utils' -import { Ledger, LedgerError } from '../../utils/ledger' +import { LedgerError, LedgerSingleSigner } from '../../utils/ledger' export class ImportCommand extends IronfishCommand { static description = `import an account` @@ -118,7 +118,7 @@ export class ImportCommand extends IronfishCommand { async importLedger(): Promise { try { - const ledger = new Ledger(this.logger) + const ledger = new LedgerSingleSigner(this.logger) await ledger.connect() const account = await ledger.importAccount() return encodeAccountImport(account, AccountFormat.Base64Json) diff --git a/ironfish-cli/src/commands/wallet/transactions/sign.ts b/ironfish-cli/src/commands/wallet/transactions/sign.ts index 87b57ca0b8..2a073e5c21 100644 --- a/ironfish-cli/src/commands/wallet/transactions/sign.ts +++ b/ironfish-cli/src/commands/wallet/transactions/sign.ts @@ -7,7 +7,7 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../command' import { RemoteFlags } from '../../../flags' import * as ui from '../../../ui' -import { Ledger } from '../../../utils/ledger' +import { LedgerSingleSigner } from '../../../utils/ledger' import { renderTransactionDetails, watchTransaction } from '../../../utils/transaction' export class TransactionsSignCommand extends IronfishCommand { @@ -109,7 +109,7 @@ export class TransactionsSignCommand extends IronfishCommand { } private async signWithLedger(client: RpcClient, unsignedTransaction: string) { - const ledger = new Ledger(this.logger) + const ledger = new LedgerSingleSigner(this.logger) try { await ledger.connect() } catch (e) { diff --git a/ironfish-cli/src/utils/ledger.ts b/ironfish-cli/src/utils/ledger.ts index 106051fab3..2fff4c89ed 100644 --- a/ironfish-cli/src/utils/ledger.ts +++ b/ironfish-cli/src/utils/ledger.ts @@ -17,36 +17,32 @@ import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' import { Errors, ux } from '@oclif/core' import IronfishApp, { IronfishKeys, + KeyResponse, ResponseAddress, + ResponseDkgRound1, + ResponseDkgRound2, + ResponseIdentity, ResponseProofGenKey, ResponseSign, ResponseViewKey, } from '@zondax/ledger-ironfish' -import { - default as IronfishDkgApp, - KeyResponse, - ResponseAddress as ResponseAddressDkg, - ResponseDkgRound1, - ResponseDkgRound2, - ResponseIdentity, - ResponseProofGenKey as ResponseProofGenKeyDkg, - ResponseViewKey as ResponseViewKeyDkg, -} from '@zondax/ledger-ironfish-dkg' import { ResponseError } from '@zondax/ledger-js' import * as ui from '../ui' import { watchTransaction } from './transaction' -export class LedgerDkg { - app: IronfishDkgApp | undefined +class LedgerBase { + app: IronfishApp | undefined logger: Logger PATH = "m/44'/1338'/0" + isDkg: boolean - constructor(logger?: Logger) { + constructor(isDkg: boolean, logger?: Logger) { this.app = undefined this.logger = logger ? logger : createRootLogger() + this.isDkg = isDkg } - tryInstruction = async (instruction: (app: IronfishDkgApp) => Promise) => { + tryInstruction = async (instruction: (app: IronfishApp) => Promise) => { await this.refreshConnection() Assert.isNotUndefined(this.app, 'Unable to establish connection with Ledger device') @@ -80,7 +76,7 @@ export class LedgerDkg { this.logger.debug(`${transport.deviceModel.productName} found.`) } - const app = new IronfishDkgApp(transport, true) + const app = new IronfishApp(transport, this.isDkg) // If the app isn't open or the device is locked, this will throw an error. await app.getVersion() @@ -90,11 +86,17 @@ export class LedgerDkg { return { app, PATH: this.PATH } } - private refreshConnection = async () => { + protected refreshConnection = async () => { if (!this.app) { await this.connect() } } +} + +export class LedgerDkg extends LedgerBase { + constructor(logger?: Logger) { + super(true, logger) + } dkgGetIdentity = async (index: number): Promise => { this.logger.log('Retrieving identity from ledger device.') @@ -191,20 +193,12 @@ export class LedgerDkg { } dkgGetPublicPackage = async (): Promise => { - if (!this.app) { - throw new Error('Connect to Ledger first') - } - const response = await this.tryInstruction((app) => app.dkgGetPublicPackage()) return response.publicPackage } reviewTransaction = async (transaction: string): Promise => { - if (!this.app) { - throw new Error('Connect to Ledger first') - } - this.logger.info( 'Please review and approve the outputs of this transaction on your ledger device.', ) @@ -215,10 +209,6 @@ export class LedgerDkg { } dkgGetCommitments = async (transactionHash: string): Promise => { - if (!this.app) { - throw new Error('Connect to Ledger first') - } - const { commitments } = await this.tryInstruction((app) => app.dkgGetCommitments(transactionHash), ) @@ -231,10 +221,6 @@ export class LedgerDkg { frostSigningPackage: string, transactionHash: string, ): Promise => { - if (!this.app) { - throw new Error('Connect to Ledger first') - } - const { signature } = await this.tryInstruction((app) => app.dkgSign(randomness, frostSigningPackage, transactionHash), ) @@ -243,10 +229,6 @@ export class LedgerDkg { } dkgBackupKeys = async (): Promise => { - if (!this.app) { - throw new Error('Connect to Ledger first') - } - this.logger.log('Please approve the request on your ledger device.') const { encryptedKeys } = await this.tryInstruction((app) => app.dkgBackupKeys()) @@ -255,131 +237,57 @@ export class LedgerDkg { } dkgRestoreKeys = async (encryptedKeys: string): Promise => { - if (!this.app) { - throw new Error('Connect to Ledger first') - } - this.logger.log('Please approve the request on your ledger device.') await this.tryInstruction((app) => app.dkgRestoreKeys(encryptedKeys)) } } -export class Ledger { - app: IronfishApp | undefined - logger: Logger - PATH = "m/44'/1338'/0" - +export class LedgerSingleSigner extends LedgerBase { constructor(logger?: Logger) { - this.app = undefined - this.logger = logger ? logger : createRootLogger() - } - - connect = async () => { - const transport = await TransportNodeHid.create(3000) - - if (transport.deviceModel) { - this.logger.debug(`${transport.deviceModel.productName} found.`) - } - - const app = new IronfishApp(transport) - - const appInfo = await app.appInfo() - this.logger.debug(appInfo.appName ?? 'no app name') - - if (appInfo.appName !== 'Ironfish') { - this.logger.debug(appInfo.appName ?? 'no app name') - this.logger.debug(appInfo.returnCode.toString(16)) - this.logger.debug(appInfo.errorMessage.toString()) - - // references: - // https://github.com/LedgerHQ/ledger-live/blob/173bb3c84cc855f83ab8dc49362bc381afecc31e/libs/ledgerjs/packages/errors/src/index.ts#L263 - // https://github.com/Zondax/ledger-ironfish/blob/bf43a4b8d403d15138699ee3bb1a3d6dfdb428bc/docs/APDUSPEC.md?plain=1#L25 - if (appInfo.returnCode === 0x5515) { - throw new LedgerError('Please unlock your Ledger device.') - } - - throw new LedgerError('Please open the Iron Fish app on your ledger device.') - } - - if (appInfo.appVersion) { - this.logger.debug(`Ironfish App Version: ${appInfo.appVersion}`) - } - - this.app = app - - return { app, PATH: this.PATH } + super(false, logger) } getPublicAddress = async () => { - if (!this.app) { - throw new Error('Connect to Ledger first') - } - - const response: ResponseAddress = await this.app.retrieveKeys( - this.PATH, - IronfishKeys.PublicAddress, - false, + const response: KeyResponse = await this.tryInstruction((app) => + app.retrieveKeys(this.PATH, IronfishKeys.PublicAddress, false), ) - if (!response.publicAddress) { - this.logger.debug(`No public address returned.`) - this.logger.debug(response.returnCode.toString()) - throw new Error(response.errorMessage) + if (!isResponseAddress(response)) { + throw new Error(`No public address returned.`) } return response.publicAddress.toString('hex') } importAccount = async () => { - if (!this.app) { - throw new Error('Connect to Ledger first') - } - - const responseAddress: ResponseAddress = await this.app.retrieveKeys( - this.PATH, - IronfishKeys.PublicAddress, - false, - ) - - if (!responseAddress.publicAddress) { - this.logger.debug(`No public address returned.`) - this.logger.debug(responseAddress.returnCode.toString()) - throw new Error(responseAddress.errorMessage) - } + const publicAddress = await this.getPublicAddress() this.logger.log('Please confirm the request on your ledger device.') - const responseViewKey: ResponseViewKey = await this.app.retrieveKeys( - this.PATH, - IronfishKeys.ViewKey, - true, + const responseViewKey: KeyResponse = await this.tryInstruction((app) => + app.retrieveKeys(this.PATH, IronfishKeys.ViewKey, true), ) - if (!responseViewKey.viewKey || !responseViewKey.ovk || !responseViewKey.ivk) { - this.logger.debug(`No view key returned.`) - this.logger.debug(responseViewKey.returnCode.toString()) - throw new Error(responseViewKey.errorMessage) + if (!isResponseViewKey(responseViewKey)) { + throw new Error(`No view key returned.`) } - const responsePGK: ResponseProofGenKey = await this.app.retrieveKeys( - this.PATH, - IronfishKeys.ProofGenerationKey, - false, + const responsePGK: KeyResponse = await this.tryInstruction((app) => + app.retrieveKeys(this.PATH, IronfishKeys.ProofGenerationKey, false), ) - if (!responsePGK.ak || !responsePGK.nsk) { - this.logger.debug(`No proof authorizing key returned.`) - throw new Error(responsePGK.errorMessage) + if (!isResponseProofGenKey(responsePGK)) { + throw new Error(`No proof authorizing key returned.`) } const accountImport: AccountImport = { version: ACCOUNT_SCHEMA_VERSION, name: 'ledger', + publicAddress, viewKey: responseViewKey.viewKey.toString('hex'), incomingViewKey: responseViewKey.ivk.toString('hex'), outgoingViewKey: responseViewKey.ovk.toString('hex'), - publicAddress: responseAddress.publicAddress.toString('hex'), proofAuthorizingKey: responsePGK.nsk.toString('hex'), spendingKey: null, createdAt: null, @@ -389,12 +297,6 @@ export class Ledger { } sign = async (message: string): Promise => { - if (!this.app) { - throw new Error('Connect to Ledger first') - } - - this.logger.log('Please confirm the request on your ledger device.') - const buffer = Buffer.from(message, 'hex') // max size of a transaction is 16kb @@ -402,28 +304,24 @@ export class Ledger { throw new Error('Transaction size is too large, must be less than 16kb.') } - const response: ResponseSign = await this.app.sign(this.PATH, buffer) - - if (!response.signature) { - this.logger.debug(`No signatures returned.`) - this.logger.debug(response.returnCode.toString()) - throw new Error(response.errorMessage) - } + const response: ResponseSign = await this.tryInstruction((app) => + app.sign(this.PATH, buffer), + ) return response.signature } } -function isResponseAddress(response: KeyResponse): response is ResponseAddressDkg { +function isResponseAddress(response: KeyResponse): response is ResponseAddress { return 'publicAddress' in response } -function isResponseViewKey(response: KeyResponse): response is ResponseViewKeyDkg { +function isResponseViewKey(response: KeyResponse): response is ResponseViewKey { return 'viewKey' in response } -function isResponseProofGenKey(response: KeyResponse): response is ResponseProofGenKeyDkg { - return 'ak' in response +function isResponseProofGenKey(response: KeyResponse): response is ResponseProofGenKey { + return 'ak' in response && 'nsk' in response } function isResponseError(error: unknown): error is ResponseError { @@ -460,7 +358,7 @@ export async function sendTransactionWithLedger( confirm: boolean, logger?: Logger, ): Promise { - const ledger = new Ledger(logger) + const ledger = new LedgerSingleSigner(logger) try { await ledger.connect() } catch (e) { diff --git a/yarn.lock b/yarn.lock index dc9a6999ee..68660cac83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1495,16 +1495,6 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@ledgerhq/devices@^8.0.0", "@ledgerhq/devices@^8.4.2": - version "8.4.3" - resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-8.4.3.tgz#4c296df4dd4af6f1085d728609b6931a640baf86" - integrity sha512-+ih+M27E6cm6DHrmw3GbS3mEaznCyFc0e62VdQux40XK2psgYhL2yBPftM4KCrBYm1UbHqXzqLN+Jb7rNIzsHg== - dependencies: - "@ledgerhq/errors" "^6.19.0" - "@ledgerhq/logs" "^6.12.0" - rxjs "^7.8.1" - semver "^7.3.5" - "@ledgerhq/devices@^8.4.0": version "8.4.0" resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-8.4.0.tgz#f3a03576d4a53d731bdaa212a00bd0adbfb86fb1" @@ -1515,16 +1505,26 @@ rxjs "^7.8.1" semver "^7.3.5" -"@ledgerhq/errors@^6.12.3", "@ledgerhq/errors@^6.18.0", "@ledgerhq/errors@^6.19.0": - version "6.19.0" - resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.19.0.tgz#ed4f01df3dabfcdeb0b073159d66cb5f2d086243" - integrity sha512-c3Jid7euMSnpHFp8H7iPtsmKDjwbTjlG46YKdw+RpCclsqtBx1uQDlYmcbP1Yv9201kVlUFUhhP4H623k8xzlQ== +"@ledgerhq/devices@^8.4.2": + version "8.4.3" + resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-8.4.3.tgz#4c296df4dd4af6f1085d728609b6931a640baf86" + integrity sha512-+ih+M27E6cm6DHrmw3GbS3mEaznCyFc0e62VdQux40XK2psgYhL2yBPftM4KCrBYm1UbHqXzqLN+Jb7rNIzsHg== + dependencies: + "@ledgerhq/errors" "^6.19.0" + "@ledgerhq/logs" "^6.12.0" + rxjs "^7.8.1" + semver "^7.3.5" "@ledgerhq/errors@^6.17.0": version "6.17.0" resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.17.0.tgz#0d56361fe6eb7de3b239e661710679f933f1fcca" integrity sha512-xnOVpy/gUUkusEORdr2Qhw3Vd0MGfjyVGgkGR9Ck6FXE26OIdIQ3tNmG5BdZN+gwMMFJJVxxS4/hr0taQfZ43w== +"@ledgerhq/errors@^6.18.0", "@ledgerhq/errors@^6.19.0": + version "6.19.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.19.0.tgz#ed4f01df3dabfcdeb0b073159d66cb5f2d086243" + integrity sha512-c3Jid7euMSnpHFp8H7iPtsmKDjwbTjlG46YKdw+RpCclsqtBx1uQDlYmcbP1Yv9201kVlUFUhhP4H623k8xzlQ== + "@ledgerhq/hw-transport-node-hid-noevents@^6.30.1": version "6.30.1" resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid-noevents/-/hw-transport-node-hid-noevents-6.30.1.tgz#e84854c809dda02bcb74a6d3dcc20b6014b5210d" @@ -1550,15 +1550,6 @@ node-hid "2.1.2" usb "2.9.0" -"@ledgerhq/hw-transport@6.28.1": - version "6.28.1" - resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-6.28.1.tgz#cb22fe9bc23af4682c30f2aac7fe6f7ab13ed65a" - integrity sha512-RaZe+abn0zBIz82cE9tp7Y7aZkHWWbEaE2yJpfxT8AhFz3fx+BU0kLYzuRN9fmA7vKueNJ1MTVUCY+Ex9/CHSQ== - dependencies: - "@ledgerhq/devices" "^8.0.0" - "@ledgerhq/errors" "^6.12.3" - events "^3.3.0" - "@ledgerhq/hw-transport@6.31.2": version "6.31.2" resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-6.31.2.tgz#79c95f7928a64a0e3b5bc4ea7b5be04b9f738322" @@ -3911,27 +3902,13 @@ dependencies: argparse "^2.0.1" -"@zondax/ledger-ironfish-dkg@npm:@zondax/ledger-ironfish@0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@zondax/ledger-ironfish/-/ledger-ironfish-0.4.0.tgz#424d213a73688f8ec33035325d45cb0f0d7915da" - integrity sha512-ifZPJl0WKKvTxAZCGRPARRJJv+qssU6PJYZEJTPHe+Vy2GSbcpfwbIzoyLqKI1vlPBQ1InbZYBP5BOmU1zRWnQ== +"@zondax/ledger-ironfish@0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@zondax/ledger-ironfish/-/ledger-ironfish-0.5.1.tgz#c628a625d2f66280c74fc2859c70ac059451e8e4" + integrity sha512-yzoyejbz5kRFSD3D3u2pIkEOwmAWWKBXiVr7ssb5TRXdimLUTHFgST7CIMp1iqRrkw8bw6HcM7RKcGol5bd9xQ== dependencies: "@zondax/ledger-js" "^1.0.1" -"@zondax/ledger-ironfish@0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@zondax/ledger-ironfish/-/ledger-ironfish-0.1.2.tgz#2ff93139c706734eb0d6800f743a9e0c2ae5268d" - integrity sha512-a9qnSOHxAf76pMonJBy5jI9oauR2W7WpVu/cCBs151uEW78NeSu4IMHOLGCo8KNiTPzpGwGa/7+1bpzxlQiEng== - dependencies: - "@zondax/ledger-js" "^0.2.1" - -"@zondax/ledger-js@^0.2.1": - version "0.2.2" - resolved "https://registry.yarnpkg.com/@zondax/ledger-js/-/ledger-js-0.2.2.tgz#b334cecaa372a8bfb91ae4fc5dd0d1c52411da4e" - integrity sha512-7wOUlRF2+kRaRU2KSzKb7XjPfScwEg3Cjg6NH/p+ikQLJ9eMkGC45NhSxYn8lixIIk+TgZ4yzTNOzFvF836gQw== - dependencies: - "@ledgerhq/hw-transport" "6.28.1" - "@zondax/ledger-js@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@zondax/ledger-js/-/ledger-js-1.0.1.tgz#a1c51943c5b7d1370cea588b193197234485d196" From 9b947749ded311e0fdf748afaab5d8bcea3e729b Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Wed, 2 Oct 2024 13:58:39 -0700 Subject: [PATCH 10/73] Adding zondax/ledger-js dependency (#5466) zondax/ledger-js is a subdependency of @zondax/ledger-ironfish. We rely on the ResponseError type from ledger-js which should ideally be exported from zondax/ledger-ironfish. This way we don't have to explicitly include this subdependency in our package.json. A todo has been added to remove this dependency when the ResponseError type is exported from ledger-ironfish. --- ironfish-cli/package.json | 1 + ironfish-cli/src/utils/ledger.ts | 2 +- yarn.lock | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index 707c10b567..d5cae5ea67 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -70,6 +70,7 @@ "@types/keccak": "3.0.4", "@types/tar": "6.1.1", "@zondax/ledger-ironfish": "0.5.1", + "@zondax/ledger-js": "1.0.1", "axios": "1.7.2", "bech32": "2.0.0", "blessed": "0.1.81", diff --git a/ironfish-cli/src/utils/ledger.ts b/ironfish-cli/src/utils/ledger.ts index 2fff4c89ed..fec0fffb5b 100644 --- a/ironfish-cli/src/utils/ledger.ts +++ b/ironfish-cli/src/utils/ledger.ts @@ -26,7 +26,7 @@ import IronfishApp, { ResponseSign, ResponseViewKey, } from '@zondax/ledger-ironfish' -import { ResponseError } from '@zondax/ledger-js' +import { ResponseError } from '@zondax/ledger-js' // todo: ResponseError will be exported from @zondax/ledger-ironfish in the future. Remove this line when it happens. import * as ui from '../ui' import { watchTransaction } from './transaction' diff --git a/yarn.lock b/yarn.lock index 68660cac83..289ef0150b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3909,7 +3909,7 @@ dependencies: "@zondax/ledger-js" "^1.0.1" -"@zondax/ledger-js@^1.0.1": +"@zondax/ledger-js@1.0.1", "@zondax/ledger-js@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@zondax/ledger-js/-/ledger-js-1.0.1.tgz#a1c51943c5b7d1370cea588b193197234485d196" integrity sha512-9h+aIXyEK+Rdic5Ppsmq+tptDFwPTacG1H6tpZHFdhtBFHYFOLLkKTTmq5rMTv84aAPS1v0tnsF1e2Il6M05Cg== From 6d1f4a88e90fbe86b4c84ad4ab9f64a28a57ca6e Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:06:51 -0700 Subject: [PATCH 11/73] multisig server signing (#5464) * multisig server signing defines common interface for storing dkg sessions and signing sessions in server memory adds network messages for signing flow: starting a signing session, signing commitments, signature shares, and retrieving the status of a signing session updates server and client implementations for new message types updates 'sign' command: - adds '--server' flag - adds '--sessionId' flag - prompts user to enter a session id or start a new session if server is set, but not session id - connects to server, starts or joins a session, and uses messages to/from server to complete signing * Update ironfish-cli/src/commands/wallet/multisig/sign.ts Co-authored-by: mat-if <97762857+mat-if@users.noreply.github.com> * clears correct event listeners in sign command --------- Co-authored-by: mat-if <97762857+mat-if@users.noreply.github.com> --- .../src/commands/wallet/multisig/sign.ts | 252 ++++++++++++++--- .../utils/multisig/network/clients/client.ts | 62 +++++ .../src/utils/multisig/network/messages.ts | 53 ++++ .../src/utils/multisig/network/server.ts | 254 ++++++++++++++++-- 4 files changed, 561 insertions(+), 60 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts index 4f75574b05..c563530a99 100644 --- a/ironfish-cli/src/commands/wallet/multisig/sign.ts +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -4,17 +4,22 @@ import { multisig } from '@ironfish/rust-nodejs' import { + Assert, CurrencyUtils, Identity, + parseUrl, + PromiseUtils, RpcClient, Transaction, UnsignedTransaction, } from '@ironfish/sdk' import { Flags, ux } from '@oclif/core' +import dns from 'dns' import { IronfishCommand } from '../../../command' import { RemoteFlags } from '../../../flags' import * as ui from '../../../ui' import { LedgerDkg } from '../../../utils/ledger' +import { MultisigTcpClient } from '../../../utils/multisig/network' import { renderUnsignedTransactionDetails, watchTransaction } from '../../../utils/transaction' // todo(patnir): this command does not differentiate between a participant and an account. @@ -43,6 +48,13 @@ export class SignMultisigTransactionCommand extends IronfishCommand { default: false, description: 'Perform operation with a ledger device', }), + server: Flags.string({ + description: "multisig server to connect to. formatted as ':'", + }), + sessionId: Flags.string({ + description: 'Unique ID for a multisig server session to join', + dependsOn: ['server'], + }), } async start(): Promise { @@ -103,12 +115,38 @@ export class SignMultisigTransactionCommand extends IronfishCommand { ) } - const unsignedTransactionInput = - flags.unsignedTransaction ?? - (await ui.longPrompt('Enter the unsigned transaction', { required: true })) - const unsignedTransaction = new UnsignedTransaction( - Buffer.from(unsignedTransactionInput, 'hex'), + let multisigClient: MultisigTcpClient | null = null + if (flags.server) { + const parsed = parseUrl(flags.server) + + Assert.isNotNull(parsed.hostname) + Assert.isNotNull(parsed.port) + + const resolved = await dns.promises.lookup(parsed.hostname) + const host = resolved.address + const port = parsed.port + + multisigClient = new MultisigTcpClient({ host, port, logger: this.logger }) + multisigClient.start() + + let sessionId = flags.sessionId + if (!sessionId) { + sessionId = await ui.inputPrompt( + 'Enter the ID of a multisig session to join, or press enter to start a new session', + false, + ) + } + + if (sessionId) { + multisigClient.joinSession(sessionId) + } + } + + const { unsignedTransaction, totalParticipants } = await this.getSigningConfig( + multisigClient, + flags.unsignedTransaction, ) + await renderUnsignedTransactionDetails( client, unsignedTransaction, @@ -120,10 +158,11 @@ export class SignMultisigTransactionCommand extends IronfishCommand { async () => { return this.performCreateSigningCommitment( client, + multisigClient, multisigAccountName, participant, + totalParticipants, unsignedTransaction, - unsignedTransactionInput, ledger, ) }, @@ -141,9 +180,11 @@ export class SignMultisigTransactionCommand extends IronfishCommand { const signingPackage = await ui.retryStep(() => { return this.performAggregateCommitments( client, + multisigClient, multisigAccountName, commitment, identities, + totalParticipants, unsignedTransaction, ) }, this.logger) @@ -178,6 +219,7 @@ export class SignMultisigTransactionCommand extends IronfishCommand { () => this.performAggregateSignatures( client, + multisigClient, multisigAccountName, signingPackage, signatureShare, @@ -186,26 +228,106 @@ export class SignMultisigTransactionCommand extends IronfishCommand { this.logger, ) - this.log('Mutlisignature sign process completed!') + this.log('Multisignature sign process completed!') + multisigClient?.stop() + } + + async getSigningConfig( + multisigClient: MultisigTcpClient | null, + unsignedTransactionFlag?: string, + ): Promise<{ unsignedTransaction: UnsignedTransaction; totalParticipants: number }> { + if (multisigClient?.sessionId) { + let totalParticipants = 0 + let unsignedTransactionHex = '' + let waiting = true + multisigClient.onSigningStatus.on((message) => { + totalParticipants = message.numSigners + unsignedTransactionHex = message.unsignedTransaction + waiting = false + }) + multisigClient.getSigningStatus() + + ux.action.start('Waiting for signer config from server') + while (waiting) { + await PromiseUtils.sleep(3000) + } + multisigClient.onSigningStatus.clear() + ux.action.stop() + + const unsignedTransaction = new UnsignedTransaction( + Buffer.from(unsignedTransactionHex, 'hex'), + ) + + return { totalParticipants, unsignedTransaction } + } + + const unsignedTransactionInput = + unsignedTransactionFlag ?? + (await ui.longPrompt('Enter the unsigned transaction', { required: true })) + const unsignedTransaction = new UnsignedTransaction( + Buffer.from(unsignedTransactionInput, 'hex'), + ) + + const input = await ui.inputPrompt( + 'Enter the number of participants in signing this transaction', + true, + ) + const totalParticipants = parseInt(input) + + if (totalParticipants < 2) { + this.error('Minimum number of participants must be at least 2') + } + + if (multisigClient) { + multisigClient.startSigningSession(totalParticipants, unsignedTransactionInput) + this.log(`Started new signing session with ID ${multisigClient.sessionId}`) + } + + return { unsignedTransaction, totalParticipants } } private async performAggregateSignatures( client: RpcClient, + multisigClient: MultisigTcpClient | null, accountName: string, signingPackage: string, signatureShare: string, totalParticipants: number, ): Promise { - this.log( - `Enter ${ - totalParticipants - 1 - } signature shares of the participants (excluding your own)`, - ) + let signatureShares: string[] = [] + if (!multisigClient) { + this.log( + `Enter ${ + totalParticipants - 1 + } signature shares of the participants (excluding your own)`, + ) - const signatureShares = await ui.collectStrings('Signature Share', totalParticipants - 1, { - additionalStrings: [signatureShare], - errorOnDuplicate: true, - }) + signatureShares = await ui.collectStrings('Signature Share', totalParticipants - 1, { + additionalStrings: [signatureShare], + errorOnDuplicate: true, + }) + } else { + multisigClient.submitSignatureShare(signatureShare) + + multisigClient.onSigningStatus.on((message) => { + signatureShares = message.signatureShares + }) + multisigClient.onSignatureShare.on((message) => { + if (!signatureShares.includes(message.signatureShare)) { + signatureShares.push(message.signatureShare) + } + }) + + ux.action.start('Waiting for other Signature Shares from server') + while (signatureShares.length < totalParticipants) { + multisigClient.getSigningStatus() + await PromiseUtils.sleep(3000) + } + + multisigClient.onSigningStatus.clear() + multisigClient.onSignatureShare.clear() + ux.action.stop() + } const broadcast = await ui.confirmPrompt('Do you want to broadcast the transaction?') const watch = await ui.confirmPrompt('Do you want to watch the transaction?') @@ -289,19 +411,45 @@ export class SignMultisigTransactionCommand extends IronfishCommand { private async performAggregateCommitments( client: RpcClient, + multisigClient: MultisigTcpClient | null, accountName: string, commitment: string, identities: string[], + totalParticipants: number, unsignedTransaction: UnsignedTransaction, ) { - this.log( - `Enter ${identities.length - 1} commitments of the participants (excluding your own)`, - ) + let commitments: string[] = [] + if (!multisigClient) { + this.log( + `Enter ${identities.length - 1} commitments of the participants (excluding your own)`, + ) - const commitments = await ui.collectStrings('Commitment', identities.length - 1, { - additionalStrings: [commitment], - errorOnDuplicate: true, - }) + commitments = await ui.collectStrings('Commitment', identities.length - 1, { + additionalStrings: [commitment], + errorOnDuplicate: true, + }) + } else { + multisigClient.submitSigningCommitment(commitment) + + multisigClient.onSigningStatus.on((message) => { + commitments = message.signingCommitments + }) + multisigClient.onSigningCommitment.on((message) => { + if (!commitments.includes(message.signingCommitment)) { + commitments.push(message.signingCommitment) + } + }) + + ux.action.start('Waiting for other Signing Commitments from server') + while (commitments.length < totalParticipants) { + multisigClient.getSigningStatus() + await PromiseUtils.sleep(3000) + } + + multisigClient.onSigningStatus.clear() + multisigClient.onSigningCommitment.clear() + ux.action.stop() + } const signingPackageResponse = await client.wallet.multisig.createSigningPackage({ account: accountName, @@ -314,38 +462,54 @@ export class SignMultisigTransactionCommand extends IronfishCommand { private async performCreateSigningCommitment( client: RpcClient, + multisigClient: MultisigTcpClient | null, accountName: string, participant: MultisigParticipant, + totalParticipants: number, unsignedTransaction: UnsignedTransaction, - unsignedTransactionInput: string, ledger: LedgerDkg | undefined, ) { - this.log(`Identity for ${participant.name}: \n${participant.identity} \n`) - this.log('Share your participant identity with other signers.') + let identities: string[] = [] + if (!multisigClient) { + this.log(`Identity for ${participant.name}: \n${participant.identity} \n`) + this.log('Share your participant identity with other signers.') - const input = await ui.inputPrompt( - 'Enter the number of participants in signing this transaction', - true, - ) - const totalParticipants = parseInt(input) + this.log( + `Enter ${totalParticipants - 1} identities of the participants (excluding your own)`, + ) - if (totalParticipants < 2) { - this.error('Minimum number of participants must be at least 2') - } + identities = await ui.collectStrings('Participant Identity', totalParticipants - 1, { + additionalStrings: [participant.identity], + errorOnDuplicate: true, + }) + } else { + multisigClient.submitIdentity(participant.identity) - this.log( - `Enter ${totalParticipants - 1} identities of the participants (excluding your own)`, - ) + multisigClient.onSigningStatus.on((message) => { + identities = message.identities + }) + multisigClient.onIdentity.on((message) => { + if (!identities.includes(message.identity)) { + identities.push(message.identity) + } + }) - const identities = await ui.collectStrings('Participant Identity', totalParticipants - 1, { - additionalStrings: [participant.identity], - errorOnDuplicate: true, - }) + ux.action.start('Waiting for other Identities from server') + while (identities.length < totalParticipants) { + multisigClient.getSigningStatus() + await PromiseUtils.sleep(3000) + } - let commitment + multisigClient.onSigningStatus.clear() + multisigClient.onIdentity.clear() + ux.action.stop() + } + const unsignedTransactionHex = unsignedTransaction.serialize().toString('hex') + + let commitment if (ledger) { - await ledger.reviewTransaction(unsignedTransaction.serialize().toString('hex')) + await ledger.reviewTransaction(unsignedTransactionHex) commitment = await this.createSigningCommitmentWithLedger( ledger, @@ -357,7 +521,7 @@ export class SignMultisigTransactionCommand extends IronfishCommand { commitment = ( await client.wallet.multisig.createSigningCommitment({ account: accountName, - unsignedTransaction: unsignedTransactionInput, + unsignedTransaction: unsignedTransactionHex, signers: identities.map((identity) => ({ identity })), }) ).content.commitment diff --git a/ironfish-cli/src/utils/multisig/network/clients/client.ts b/ironfish-cli/src/utils/multisig/network/clients/client.ts index 2155d02b7a..65e396ece9 100644 --- a/ironfish-cli/src/utils/multisig/network/clients/client.ts +++ b/ironfish-cli/src/utils/multisig/network/clients/client.ts @@ -23,6 +23,14 @@ import { Round1PublicPackageSchema, Round2PublicPackageMessage, Round2PublicPackageSchema, + SignatureShareMessage, + SignatureShareSchema, + SigningCommitmentMessage, + SigningCommitmentSchema, + SigningGetStatusMessage, + SigningStartSessionMessage, + SigningStatusMessage, + SigningStatusSchema, StratumMessage, StratumMessageSchema, StratumMessageWithError, @@ -48,6 +56,9 @@ export abstract class MultisigClient { readonly onRound1PublicPackage = new Event<[Round1PublicPackageMessage]>() readonly onRound2PublicPackage = new Event<[Round2PublicPackageMessage]>() readonly onDkgStatus = new Event<[DkgStatusMessage]>() + readonly onSigningCommitment = new Event<[SigningCommitmentMessage]>() + readonly onSignatureShare = new Event<[SignatureShareMessage]>() + readonly onSigningStatus = new Event<[SigningStatusMessage]>() readonly onStratumError = new Event<[StratumMessageWithError]>() sessionId: string | null = null @@ -133,6 +144,11 @@ export abstract class MultisigClient { this.send('dkg.start_session', { maxSigners, minSigners }) } + startSigningSession(numSigners: number, unsignedTransaction: string): void { + this.sessionId = uuid() + this.send('sign.start_session', { numSigners, unsignedTransaction }) + } + submitIdentity(identity: string): void { this.send('identity', { identity }) } @@ -149,12 +165,28 @@ export abstract class MultisigClient { this.send('dkg.get_status', {}) } + submitSigningCommitment(signingCommitment: string): void { + this.send('sign.commitment', { signingCommitment }) + } + + submitSignatureShare(signatureShare: string): void { + this.send('sign.share', { signatureShare }) + } + + getSigningStatus(): void { + this.send('sign.get_status', {}) + } + private send(method: 'join_session', body: JoinSessionMessage): void private send(method: 'dkg.start_session', body: DkgStartSessionMessage): void + private send(method: 'sign.start_session', body: SigningStartSessionMessage): void private send(method: 'identity', body: IdentityMessage): void private send(method: 'dkg.round1', body: Round1PublicPackageMessage): void private send(method: 'dkg.round2', body: Round2PublicPackageMessage): void private send(method: 'dkg.get_status', body: DkgGetStatusMessage): void + private send(method: 'sign.commitment', body: SigningCommitmentMessage): void + private send(method: 'sign.share', body: SignatureShareMessage): void + private send(method: 'sign.get_status', body: SigningGetStatusMessage): void private send(method: string, body?: unknown): void { if (!this.sessionId) { throw new Error('Client must join a session before sending messages') @@ -261,6 +293,36 @@ export abstract class MultisigClient { this.onDkgStatus.emit(body.result) break } + case 'sign.commitment': { + const body = await YupUtils.tryValidate(SigningCommitmentSchema, header.result.body) + + if (body.error) { + throw new ServerMessageMalformedError(body.error, header.result.method) + } + + this.onSigningCommitment.emit(body.result) + break + } + case 'sign.share': { + const body = await YupUtils.tryValidate(SignatureShareSchema, header.result.body) + + if (body.error) { + throw new ServerMessageMalformedError(body.error, header.result.method) + } + + this.onSignatureShare.emit(body.result) + break + } + case 'sign.status': { + const body = await YupUtils.tryValidate(SigningStatusSchema, header.result.body) + + if (body.error) { + throw new ServerMessageMalformedError(body.error, header.result.method) + } + + this.onSigningStatus.emit(body.result) + break + } default: throw new ServerMessageMalformedError(`Invalid message ${header.result.method}`) diff --git a/ironfish-cli/src/utils/multisig/network/messages.ts b/ironfish-cli/src/utils/multisig/network/messages.ts index f8f921b218..f10b26b668 100644 --- a/ironfish-cli/src/utils/multisig/network/messages.ts +++ b/ironfish-cli/src/utils/multisig/network/messages.ts @@ -23,6 +23,11 @@ export type DkgStartSessionMessage = { maxSigners: number } +export type SigningStartSessionMessage = { + numSigners: number + unsignedTransaction: string +} + export type JoinSessionMessage = object | undefined export type IdentityMessage = { @@ -37,6 +42,14 @@ export type Round2PublicPackageMessage = { package: string } +export type SigningCommitmentMessage = { + signingCommitment: string +} + +export type SignatureShareMessage = { + signatureShare: string +} + export type DkgGetStatusMessage = object | undefined export type DkgStatusMessage = { @@ -47,6 +60,16 @@ export type DkgStatusMessage = { round2PublicPackages: string[] } +export type SigningGetStatusMessage = object | undefined + +export type SigningStatusMessage = { + numSigners: number + unsignedTransaction: string + identities: string[] + signingCommitments: string[] + signatureShares: string[] +} + export const StratumMessageSchema: yup.ObjectSchema = yup .object({ id: yup.number().required(), @@ -75,6 +98,13 @@ export const DkgStartSessionSchema: yup.ObjectSchema = y }) .defined() +export const SigningStartSessionSchema: yup.ObjectSchema = yup + .object({ + numSigners: yup.number().defined(), + unsignedTransaction: yup.string().defined(), + }) + .defined() + export const JoinSessionSchema: yup.ObjectSchema = yup .object({}) .notRequired() @@ -94,6 +124,14 @@ export const Round2PublicPackageSchema: yup.ObjectSchema = yup + .object({ signingCommitment: yup.string().defined() }) + .defined() + +export const SignatureShareSchema: yup.ObjectSchema = yup + .object({ signatureShare: yup.string().defined() }) + .defined() + export const DkgGetStatusSchema: yup.ObjectSchema = yup .object({}) .notRequired() @@ -108,3 +146,18 @@ export const DkgStatusSchema: yup.ObjectSchema = yup round2PublicPackages: yup.array(yup.string().defined()).defined(), }) .defined() + +export const SigningGetStatusSchema: yup.ObjectSchema = yup + .object({}) + .notRequired() + .default(undefined) + +export const SigningStatusSchema: yup.ObjectSchema = yup + .object({ + numSigners: yup.number().defined(), + unsignedTransaction: yup.string().defined(), + identities: yup.array(yup.string().defined()).defined(), + signingCommitments: yup.array(yup.string().defined()).defined(), + signatureShares: yup.array(yup.string().defined()).defined(), + }) + .defined() diff --git a/ironfish-cli/src/utils/multisig/network/server.ts b/ironfish-cli/src/utils/multisig/network/server.ts index d8f7b8a5be..a109c5c6a2 100644 --- a/ironfish-cli/src/utils/multisig/network/server.ts +++ b/ironfish-cli/src/utils/multisig/network/server.ts @@ -15,12 +15,40 @@ import { Round1PublicPackageSchema, Round2PublicPackageMessage, Round2PublicPackageSchema, + SignatureShareMessage, + SignatureShareSchema, + SigningCommitmentMessage, + SigningCommitmentSchema, + SigningGetStatusSchema, + SigningStartSessionSchema, + SigningStatusMessage, StratumMessage, StratumMessageSchema, StratumMessageWithError, } from './messages' import { MultisigServerClient } from './serverClient' +enum MultisigSessionType { + DKG = 'DKG', + SIGNING = 'SIGNING', +} + +interface MultisigSession { + id: string + type: MultisigSessionType + status: DkgStatus | SigningStatus +} + +interface DkgSession extends MultisigSession { + type: MultisigSessionType.DKG + status: DkgStatus +} + +interface SigningSession extends MultisigSession { + type: MultisigSessionType.SIGNING + status: SigningStatus +} + export type DkgStatus = { minSigners: number maxSigners: number @@ -29,6 +57,14 @@ export type DkgStatus = { round2PublicPackages: string[] } +export type SigningStatus = { + numSigners: number + unsignedTransaction: string + identities: string[] + signingCommitments: string[] + signatureShares: string[] +} + export class MultisigServer { readonly logger: Logger readonly adapters: IStratumAdapter[] = [] @@ -37,7 +73,7 @@ export class MultisigServer { nextClientId: number nextMessageId: number - sessions: Map = new Map() + sessions: Map = new Map() private _isRunning = false private _startPromise: Promise | null = null @@ -140,6 +176,9 @@ export class MultisigServer { if (message.method === 'dkg.start_session') { await this.handleDkgStartSessionMessage(client, message) return + } else if (message.method === 'sign.start_session') { + await this.handleSigningStartSessionMessage(client, message) + return } else if (message.method === 'join_session') { this.handleJoinSessionMessage(client, message) return @@ -155,6 +194,15 @@ export class MultisigServer { } else if (message.method === 'dkg.get_status') { await this.handleDkgGetStatusMessage(client, message) return + } else if (message.method === 'sign.commitment') { + await this.handleSigningCommitmentMessage(client, message) + return + } else if (message.method === 'sign.share') { + await this.handleSignatureShareMessage(client, message) + return + } else if (message.method === 'sign.get_status') { + await this.handleSigningGetStatusMessage(client, message) + return } else { throw new ClientMessageMalformedError(client, `Invalid message ${message.method}`) } @@ -186,6 +234,12 @@ export class MultisigServer { sessionId: string, body: Round2PublicPackageMessage, ): void + private broadcast( + method: 'sign.commitment', + sessionId: string, + body: SigningCommitmentMessage, + ): void + private broadcast(method: 'sign.share', sessionId: string, body: SignatureShareMessage): void private broadcast(method: string, sessionId: string, body?: unknown): void { const message: StratumMessage = { id: this.nextMessageId++, @@ -234,6 +288,12 @@ export class MultisigServer { sessionId: string, body: DkgStatusMessage, ): void + send( + socket: net.Socket, + method: 'sign.status', + sessionId: string, + body: SigningStatusMessage, + ): void send(socket: net.Socket, method: string, sessionId: string, body?: unknown): void { const message: StratumMessage = { id: this.nextMessageId++, @@ -272,15 +332,57 @@ export class MultisigServer { return } - this.sessions.set(sessionId, { - maxSigners: body.result.maxSigners, - minSigners: body.result.minSigners, - identities: [], - round1PublicPackages: [], - round2PublicPackages: [], - }) + const session = { + id: sessionId, + type: MultisigSessionType.DKG, + status: { + maxSigners: body.result.maxSigners, + minSigners: body.result.minSigners, + identities: [], + round1PublicPackages: [], + round2PublicPackages: [], + }, + } + + this.sessions.set(sessionId, session) + + this.logger.debug(`Client ${client.id} started dkg session ${message.sessionId}`) + + client.sessionId = message.sessionId + } - this.logger.debug(`Client ${client.id} started session ${message.sessionId}`) + async handleSigningStartSessionMessage( + client: MultisigServerClient, + message: StratumMessage, + ) { + const body = await YupUtils.tryValidate(SigningStartSessionSchema, message.body) + + if (body.error) { + return + } + + const sessionId = message.sessionId + + if (this.sessions.has(sessionId)) { + this.sendStratumError(client, message.id, `Duplicate sessionId: ${sessionId}`) + return + } + + const session = { + id: sessionId, + type: MultisigSessionType.SIGNING, + status: { + numSigners: body.result.numSigners, + unsignedTransaction: body.result.unsignedTransaction, + identities: [], + signingCommitments: [], + signatureShares: [], + }, + } + + this.sessions.set(sessionId, session) + + this.logger.debug(`Client ${client.id} started signing session ${message.sessionId}`) client.sessionId = message.sessionId } @@ -310,8 +412,8 @@ export class MultisigServer { } const identity = body.result.identity - if (!session.identities.includes(identity)) { - session.identities.push(identity) + if (!session.status.identities.includes(identity)) { + session.status.identities.push(identity) this.sessions.set(message.sessionId, session) this.broadcast('identity', message.sessionId, { identity }) } @@ -333,9 +435,18 @@ export class MultisigServer { return } + if (!isDkgSession(session)) { + this.sendStratumError( + client, + message.id, + `Session is not a dkg session: ${message.sessionId}`, + ) + return + } + const round1PublicPackage = body.result.package - if (!session.round1PublicPackages.includes(round1PublicPackage)) { - session.round1PublicPackages.push(round1PublicPackage) + if (!session.status.round1PublicPackages.includes(round1PublicPackage)) { + session.status.round1PublicPackages.push(round1PublicPackage) this.sessions.set(message.sessionId, session) this.broadcast('dkg.round1', message.sessionId, { package: round1PublicPackage }) } @@ -357,9 +468,18 @@ export class MultisigServer { return } + if (!isDkgSession(session)) { + this.sendStratumError( + client, + message.id, + `Session is not a dkg session: ${message.sessionId}`, + ) + return + } + const round2PublicPackage = body.result.package - if (!session.round2PublicPackages.includes(round2PublicPackage)) { - session.round2PublicPackages.push(round2PublicPackage) + if (!session.status.round2PublicPackages.includes(round2PublicPackage)) { + session.status.round2PublicPackages.push(round2PublicPackage) this.sessions.set(message.sessionId, session) this.broadcast('dkg.round2', message.sessionId, { package: round2PublicPackage }) } @@ -378,6 +498,108 @@ export class MultisigServer { return } - this.send(client.socket, 'dkg.status', message.sessionId, session) + if (!isDkgSession(session)) { + this.sendStratumError( + client, + message.id, + `Session is not a dkg session: ${message.sessionId}`, + ) + return + } + + this.send(client.socket, 'dkg.status', message.sessionId, session.status) + } + + async handleSigningCommitmentMessage(client: MultisigServerClient, message: StratumMessage) { + const body = await YupUtils.tryValidate(SigningCommitmentSchema, message.body) + + if (body.error) { + return + } + + const session = this.sessions.get(message.sessionId) + if (!session) { + this.sendStratumError(client, message.id, `Session not found: ${message.sessionId}`) + return + } + + if (!isSigningSession(session)) { + this.sendStratumError( + client, + message.id, + `Session is not a signing session: ${message.sessionId}`, + ) + return + } + + const signingCommitment = body.result.signingCommitment + if (!session.status.signingCommitments.includes(signingCommitment)) { + session.status.signingCommitments.push(signingCommitment) + this.sessions.set(message.sessionId, session) + this.broadcast('sign.commitment', message.sessionId, { signingCommitment }) + } + } + + async handleSignatureShareMessage(client: MultisigServerClient, message: StratumMessage) { + const body = await YupUtils.tryValidate(SignatureShareSchema, message.body) + + if (body.error) { + return + } + + const session = this.sessions.get(message.sessionId) + if (!session) { + this.sendStratumError(client, message.id, `Session not found: ${message.sessionId}`) + return + } + + if (!isSigningSession(session)) { + this.sendStratumError( + client, + message.id, + `Session is not a signing session: ${message.sessionId}`, + ) + return + } + + const signatureShare = body.result.signatureShare + if (!session.status.signatureShares.includes(signatureShare)) { + session.status.signatureShares.push(signatureShare) + this.sessions.set(message.sessionId, session) + this.broadcast('sign.share', message.sessionId, { signatureShare }) + } + } + + async handleSigningGetStatusMessage(client: MultisigServerClient, message: StratumMessage) { + const body = await YupUtils.tryValidate(SigningGetStatusSchema, message.body) + + if (body.error) { + return + } + + const session = this.sessions.get(message.sessionId) + if (!session) { + this.sendStratumError(client, message.id, `Session not found: ${message.sessionId}`) + return + } + + if (!isSigningSession(session)) { + this.sendStratumError( + client, + message.id, + `Session is not a signing session: ${message.sessionId}`, + ) + return + } + + this.send(client.socket, 'sign.status', message.sessionId, session.status) } } + +function isDkgSession(session: MultisigSession): session is DkgSession { + return session.type === MultisigSessionType.DKG +} + +function isSigningSession(session: MultisigSession): session is SigningSession { + return session.type === MultisigSessionType.SIGNING +} From e13f3e804aab046a0e841bd4e94cdf2737b7128c Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Wed, 2 Oct 2024 14:59:19 -0700 Subject: [PATCH 12/73] Move multisig broker into its own folder (#5467) --- ironfish-cli/src/commands/wallet/multisig/dkg/create.ts | 2 +- ironfish-cli/src/commands/wallet/multisig/server.ts | 4 ++-- ironfish-cli/src/commands/wallet/multisig/sign.ts | 2 +- ironfish-cli/src/multisigBroker/README.md | 3 +++ .../multisig/network => multisigBroker}/adapters/adapter.ts | 0 .../multisig/network => multisigBroker}/adapters/index.ts | 0 .../network => multisigBroker}/adapters/tcpAdapter.ts | 0 .../network => multisigBroker}/adapters/tlsAdapter.ts | 0 .../multisig/network => multisigBroker}/clients/client.ts | 0 .../multisig/network => multisigBroker}/clients/index.ts | 0 .../multisig/network => multisigBroker}/clients/tcpClient.ts | 0 .../multisig/network => multisigBroker}/clients/tlsClient.ts | 0 .../{utils/multisig/network => multisigBroker}/constants.ts | 0 .../src/{utils/multisig/network => multisigBroker}/errors.ts | 0 .../src/{utils/multisig/network => multisigBroker}/index.ts | 0 .../{utils/multisig/network => multisigBroker}/messages.ts | 0 .../src/{utils/multisig/network => multisigBroker}/server.ts | 0 .../multisig/network => multisigBroker}/serverClient.ts | 0 18 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 ironfish-cli/src/multisigBroker/README.md rename ironfish-cli/src/{utils/multisig/network => multisigBroker}/adapters/adapter.ts (100%) rename ironfish-cli/src/{utils/multisig/network => multisigBroker}/adapters/index.ts (100%) rename ironfish-cli/src/{utils/multisig/network => multisigBroker}/adapters/tcpAdapter.ts (100%) rename ironfish-cli/src/{utils/multisig/network => multisigBroker}/adapters/tlsAdapter.ts (100%) rename ironfish-cli/src/{utils/multisig/network => multisigBroker}/clients/client.ts (100%) rename ironfish-cli/src/{utils/multisig/network => multisigBroker}/clients/index.ts (100%) rename ironfish-cli/src/{utils/multisig/network => multisigBroker}/clients/tcpClient.ts (100%) rename ironfish-cli/src/{utils/multisig/network => multisigBroker}/clients/tlsClient.ts (100%) rename ironfish-cli/src/{utils/multisig/network => multisigBroker}/constants.ts (100%) rename ironfish-cli/src/{utils/multisig/network => multisigBroker}/errors.ts (100%) rename ironfish-cli/src/{utils/multisig/network => multisigBroker}/index.ts (100%) rename ironfish-cli/src/{utils/multisig/network => multisigBroker}/messages.ts (100%) rename ironfish-cli/src/{utils/multisig/network => multisigBroker}/server.ts (100%) rename ironfish-cli/src/{utils/multisig/network => multisigBroker}/serverClient.ts (100%) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index 34ac36da4a..d8ed15fa57 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -21,9 +21,9 @@ import fs from 'fs' import path from 'path' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' +import { MultisigTcpClient } from '../../../../multisigBroker' import * as ui from '../../../../ui' import { LedgerDkg } from '../../../../utils/ledger' -import { MultisigTcpClient } from '../../../../utils/multisig/network' export class DkgCreateCommand extends IronfishCommand { static description = 'Interactive command to create a multisignature account using DKG' diff --git a/ironfish-cli/src/commands/wallet/multisig/server.ts b/ironfish-cli/src/commands/wallet/multisig/server.ts index 5f19a8ff43..20664e6963 100644 --- a/ironfish-cli/src/commands/wallet/multisig/server.ts +++ b/ironfish-cli/src/commands/wallet/multisig/server.ts @@ -4,8 +4,8 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../command' -import { MultisigServer } from '../../../utils/multisig/network' -import { MultisigTcpAdapter } from '../../../utils/multisig/network/adapters' +import { MultisigServer } from '../../../multisigBroker' +import { MultisigTcpAdapter } from '../../../multisigBroker/adapters' export class MultisigServerCommand extends IronfishCommand { static description = 'start a server to broker messages for a multisig session' diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts index c563530a99..a4ddd7924b 100644 --- a/ironfish-cli/src/commands/wallet/multisig/sign.ts +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -17,9 +17,9 @@ import { Flags, ux } from '@oclif/core' import dns from 'dns' import { IronfishCommand } from '../../../command' import { RemoteFlags } from '../../../flags' +import { MultisigTcpClient } from '../../../multisigBroker' import * as ui from '../../../ui' import { LedgerDkg } from '../../../utils/ledger' -import { MultisigTcpClient } from '../../../utils/multisig/network' import { renderUnsignedTransactionDetails, watchTransaction } from '../../../utils/transaction' // todo(patnir): this command does not differentiate between a participant and an account. diff --git a/ironfish-cli/src/multisigBroker/README.md b/ironfish-cli/src/multisigBroker/README.md new file mode 100644 index 0000000000..91f2d80444 --- /dev/null +++ b/ironfish-cli/src/multisigBroker/README.md @@ -0,0 +1,3 @@ +# Multisig Broker + +This is a server / client that allows multisig participants to broker creating DKG accounts and signing transactions through a trustless server. diff --git a/ironfish-cli/src/utils/multisig/network/adapters/adapter.ts b/ironfish-cli/src/multisigBroker/adapters/adapter.ts similarity index 100% rename from ironfish-cli/src/utils/multisig/network/adapters/adapter.ts rename to ironfish-cli/src/multisigBroker/adapters/adapter.ts diff --git a/ironfish-cli/src/utils/multisig/network/adapters/index.ts b/ironfish-cli/src/multisigBroker/adapters/index.ts similarity index 100% rename from ironfish-cli/src/utils/multisig/network/adapters/index.ts rename to ironfish-cli/src/multisigBroker/adapters/index.ts diff --git a/ironfish-cli/src/utils/multisig/network/adapters/tcpAdapter.ts b/ironfish-cli/src/multisigBroker/adapters/tcpAdapter.ts similarity index 100% rename from ironfish-cli/src/utils/multisig/network/adapters/tcpAdapter.ts rename to ironfish-cli/src/multisigBroker/adapters/tcpAdapter.ts diff --git a/ironfish-cli/src/utils/multisig/network/adapters/tlsAdapter.ts b/ironfish-cli/src/multisigBroker/adapters/tlsAdapter.ts similarity index 100% rename from ironfish-cli/src/utils/multisig/network/adapters/tlsAdapter.ts rename to ironfish-cli/src/multisigBroker/adapters/tlsAdapter.ts diff --git a/ironfish-cli/src/utils/multisig/network/clients/client.ts b/ironfish-cli/src/multisigBroker/clients/client.ts similarity index 100% rename from ironfish-cli/src/utils/multisig/network/clients/client.ts rename to ironfish-cli/src/multisigBroker/clients/client.ts diff --git a/ironfish-cli/src/utils/multisig/network/clients/index.ts b/ironfish-cli/src/multisigBroker/clients/index.ts similarity index 100% rename from ironfish-cli/src/utils/multisig/network/clients/index.ts rename to ironfish-cli/src/multisigBroker/clients/index.ts diff --git a/ironfish-cli/src/utils/multisig/network/clients/tcpClient.ts b/ironfish-cli/src/multisigBroker/clients/tcpClient.ts similarity index 100% rename from ironfish-cli/src/utils/multisig/network/clients/tcpClient.ts rename to ironfish-cli/src/multisigBroker/clients/tcpClient.ts diff --git a/ironfish-cli/src/utils/multisig/network/clients/tlsClient.ts b/ironfish-cli/src/multisigBroker/clients/tlsClient.ts similarity index 100% rename from ironfish-cli/src/utils/multisig/network/clients/tlsClient.ts rename to ironfish-cli/src/multisigBroker/clients/tlsClient.ts diff --git a/ironfish-cli/src/utils/multisig/network/constants.ts b/ironfish-cli/src/multisigBroker/constants.ts similarity index 100% rename from ironfish-cli/src/utils/multisig/network/constants.ts rename to ironfish-cli/src/multisigBroker/constants.ts diff --git a/ironfish-cli/src/utils/multisig/network/errors.ts b/ironfish-cli/src/multisigBroker/errors.ts similarity index 100% rename from ironfish-cli/src/utils/multisig/network/errors.ts rename to ironfish-cli/src/multisigBroker/errors.ts diff --git a/ironfish-cli/src/utils/multisig/network/index.ts b/ironfish-cli/src/multisigBroker/index.ts similarity index 100% rename from ironfish-cli/src/utils/multisig/network/index.ts rename to ironfish-cli/src/multisigBroker/index.ts diff --git a/ironfish-cli/src/utils/multisig/network/messages.ts b/ironfish-cli/src/multisigBroker/messages.ts similarity index 100% rename from ironfish-cli/src/utils/multisig/network/messages.ts rename to ironfish-cli/src/multisigBroker/messages.ts diff --git a/ironfish-cli/src/utils/multisig/network/server.ts b/ironfish-cli/src/multisigBroker/server.ts similarity index 100% rename from ironfish-cli/src/utils/multisig/network/server.ts rename to ironfish-cli/src/multisigBroker/server.ts diff --git a/ironfish-cli/src/utils/multisig/network/serverClient.ts b/ironfish-cli/src/multisigBroker/serverClient.ts similarity index 100% rename from ironfish-cli/src/utils/multisig/network/serverClient.ts rename to ironfish-cli/src/multisigBroker/serverClient.ts From 554de9288918286dd9c8b3c61fa7b4465c81484e Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Wed, 2 Oct 2024 15:17:12 -0700 Subject: [PATCH 13/73] Remove references to stratum in MultisigBroker (#5468) --- .../src/multisigBroker/adapters/adapter.ts | 2 +- .../src/multisigBroker/adapters/tcpAdapter.ts | 4 +- .../src/multisigBroker/clients/client.ts | 18 ++-- ironfish-cli/src/multisigBroker/messages.ts | 31 +++--- ironfish-cli/src/multisigBroker/server.ts | 97 +++++++++++-------- 5 files changed, 84 insertions(+), 68 deletions(-) diff --git a/ironfish-cli/src/multisigBroker/adapters/adapter.ts b/ironfish-cli/src/multisigBroker/adapters/adapter.ts index 9a8500dee6..267d6ea528 100644 --- a/ironfish-cli/src/multisigBroker/adapters/adapter.ts +++ b/ironfish-cli/src/multisigBroker/adapters/adapter.ts @@ -8,7 +8,7 @@ import { MultisigServer } from '../server' * An adapter represents a network transport that accepts connections from * clients and routes them into the server. */ -export interface IStratumAdapter { +export interface IMultisigBrokerAdapter { /** * Called when the adapter is added to a MultisigServer. */ diff --git a/ironfish-cli/src/multisigBroker/adapters/tcpAdapter.ts b/ironfish-cli/src/multisigBroker/adapters/tcpAdapter.ts index c2e787f9de..e895ec7d50 100644 --- a/ironfish-cli/src/multisigBroker/adapters/tcpAdapter.ts +++ b/ironfish-cli/src/multisigBroker/adapters/tcpAdapter.ts @@ -4,9 +4,9 @@ import { Logger } from '@ironfish/sdk' import net from 'net' import { MultisigServer } from '../server' -import { IStratumAdapter } from './adapter' +import { IMultisigBrokerAdapter } from './adapter' -export class MultisigTcpAdapter implements IStratumAdapter { +export class MultisigTcpAdapter implements IMultisigBrokerAdapter { server: net.Server | null = null multisigServer: MultisigServer | null = null readonly logger: Logger diff --git a/ironfish-cli/src/multisigBroker/clients/client.ts b/ironfish-cli/src/multisigBroker/clients/client.ts index 65e396ece9..1395d11d89 100644 --- a/ironfish-cli/src/multisigBroker/clients/client.ts +++ b/ironfish-cli/src/multisigBroker/clients/client.ts @@ -19,6 +19,10 @@ import { IdentityMessage, IdentitySchema, JoinSessionMessage, + MultisigBrokerMessage, + MultisigBrokerMessageSchema, + MultisigBrokerMessageWithError, + MultisigBrokerMessageWithErrorSchema, Round1PublicPackageMessage, Round1PublicPackageSchema, Round2PublicPackageMessage, @@ -31,10 +35,6 @@ import { SigningStartSessionMessage, SigningStatusMessage, SigningStatusSchema, - StratumMessage, - StratumMessageSchema, - StratumMessageWithError, - StratumMessageWithErrorSchema, } from '../messages' export abstract class MultisigClient { @@ -59,7 +59,7 @@ export abstract class MultisigClient { readonly onSigningCommitment = new Event<[SigningCommitmentMessage]>() readonly onSignatureShare = new Event<[SignatureShareMessage]>() readonly onSigningStatus = new Event<[SigningStatusMessage]>() - readonly onStratumError = new Event<[StratumMessageWithError]>() + readonly onMultisigBrokerError = new Event<[MultisigBrokerMessageWithError]>() sessionId: string | null = null @@ -196,7 +196,7 @@ export abstract class MultisigClient { return } - const message: StratumMessage = { + const message: MultisigBrokerMessage = { id: this.nextMessageId++, method, sessionId: this.sessionId, @@ -232,12 +232,12 @@ export abstract class MultisigClient { for (const message of this.messageBuffer.readMessages()) { const payload: unknown = JSON.parse(message) - const header = await YupUtils.tryValidate(StratumMessageSchema, payload) + const header = await YupUtils.tryValidate(MultisigBrokerMessageSchema, payload) if (header.error) { // Try the error message instead. const headerWithError = await YupUtils.tryValidate( - StratumMessageWithErrorSchema, + MultisigBrokerMessageWithErrorSchema, payload, ) if (headerWithError.error) { @@ -246,7 +246,7 @@ export abstract class MultisigClient { this.logger.debug( `Server sent error ${headerWithError.result.error.message} for id ${headerWithError.result.error.id}`, ) - this.onStratumError.emit(headerWithError.result) + this.onMultisigBrokerError.emit(headerWithError.result) return } diff --git a/ironfish-cli/src/multisigBroker/messages.ts b/ironfish-cli/src/multisigBroker/messages.ts index f10b26b668..d0c19bd296 100644 --- a/ironfish-cli/src/multisigBroker/messages.ts +++ b/ironfish-cli/src/multisigBroker/messages.ts @@ -3,15 +3,15 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import * as yup from 'yup' -export type StratumMessage = { +export type MultisigBrokerMessage = { id: number method: string sessionId: string body?: unknown } -export interface StratumMessageWithError - extends Omit { +export interface MultisigBrokerMessageWithError + extends Omit { error: { id: number message: string @@ -70,7 +70,7 @@ export type SigningStatusMessage = { signatureShares: string[] } -export const StratumMessageSchema: yup.ObjectSchema = yup +export const MultisigBrokerMessageSchema: yup.ObjectSchema = yup .object({ id: yup.number().required(), method: yup.string().required(), @@ -79,17 +79,18 @@ export const StratumMessageSchema: yup.ObjectSchema = yup }) .required() -export const StratumMessageWithErrorSchema: yup.ObjectSchema = yup - .object({ - id: yup.number().required(), - error: yup - .object({ - id: yup.number().required(), - message: yup.string().required(), - }) - .required(), - }) - .required() +export const MultisigBrokerMessageWithErrorSchema: yup.ObjectSchema = + yup + .object({ + id: yup.number().required(), + error: yup + .object({ + id: yup.number().required(), + message: yup.string().required(), + }) + .required(), + }) + .required() export const DkgStartSessionSchema: yup.ObjectSchema = yup .object({ diff --git a/ironfish-cli/src/multisigBroker/server.ts b/ironfish-cli/src/multisigBroker/server.ts index a109c5c6a2..bb38f79f97 100644 --- a/ironfish-cli/src/multisigBroker/server.ts +++ b/ironfish-cli/src/multisigBroker/server.ts @@ -3,7 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { ErrorUtils, Logger, YupUtils } from '@ironfish/sdk' import net from 'net' -import { IStratumAdapter } from './adapters' +import { IMultisigBrokerAdapter } from './adapters' import { ClientMessageMalformedError } from './errors' import { DkgGetStatusSchema, @@ -11,6 +11,9 @@ import { DkgStatusMessage, IdentityMessage, IdentitySchema, + MultisigBrokerMessage, + MultisigBrokerMessageSchema, + MultisigBrokerMessageWithError, Round1PublicPackageMessage, Round1PublicPackageSchema, Round2PublicPackageMessage, @@ -22,9 +25,6 @@ import { SigningGetStatusSchema, SigningStartSessionSchema, SigningStatusMessage, - StratumMessage, - StratumMessageSchema, - StratumMessageWithError, } from './messages' import { MultisigServerClient } from './serverClient' @@ -67,7 +67,7 @@ export type SigningStatus = { export class MultisigServer { readonly logger: Logger - readonly adapters: IStratumAdapter[] = [] + readonly adapters: IMultisigBrokerAdapter[] = [] clients: Map nextClientId: number @@ -90,7 +90,7 @@ export class MultisigServer { return this._isRunning } - /** Starts the Stratum server and tells any attached adapters to start serving requests */ + /** Starts the MultisigBroker server and tells any attached adapters to start serving requests */ async start(): Promise { if (this._isRunning) { return @@ -101,7 +101,7 @@ export class MultisigServer { await this._startPromise } - /** Stops the Stratum server and tells any attached adapters to stop serving requests */ + /** Stops the MultisigBroker server and tells any attached adapters to stop serving requests */ async stop(): Promise { if (!this._isRunning) { return @@ -115,8 +115,8 @@ export class MultisigServer { this._isRunning = false } - /** Adds an adapter to the Stratum server and starts it if the server has already been started */ - mount(adapter: IStratumAdapter): void { + /** Adds an adapter to the MultisigBroker server and starts it if the server has already been started */ + mount(adapter: IMultisigBrokerAdapter): void { this.adapters.push(adapter) adapter.attach(this) @@ -162,12 +162,12 @@ export class MultisigServer { for (const split of client.messageBuffer.readMessages()) { const payload: unknown = JSON.parse(split) const { error: parseError, result: message } = await YupUtils.tryValidate( - StratumMessageSchema, + MultisigBrokerMessageSchema, payload, ) if (parseError) { - this.sendStratumError(client, 0, `Error parsing message`) + this.sendErrorMessage(client, 0, `Error parsing message`) return } @@ -241,7 +241,7 @@ export class MultisigServer { ): void private broadcast(method: 'sign.share', sessionId: string, body: SignatureShareMessage): void private broadcast(method: string, sessionId: string, body?: unknown): void { - const message: StratumMessage = { + const message: MultisigBrokerMessage = { id: this.nextMessageId++, method, sessionId, @@ -295,7 +295,7 @@ export class MultisigServer { body: SigningStatusMessage, ): void send(socket: net.Socket, method: string, sessionId: string, body?: unknown): void { - const message: StratumMessage = { + const message: MultisigBrokerMessage = { id: this.nextMessageId++, method, sessionId, @@ -306,8 +306,8 @@ export class MultisigServer { socket.write(serialized) } - sendStratumError(client: MultisigServerClient, id: number, message: string): void { - const msg: StratumMessageWithError = { + sendErrorMessage(client: MultisigServerClient, id: number, message: string): void { + const msg: MultisigBrokerMessageWithError = { id: this.nextMessageId++, error: { id: id, @@ -318,7 +318,10 @@ export class MultisigServer { client.socket.write(serialized) } - async handleDkgStartSessionMessage(client: MultisigServerClient, message: StratumMessage) { + async handleDkgStartSessionMessage( + client: MultisigServerClient, + message: MultisigBrokerMessage, + ) { const body = await YupUtils.tryValidate(DkgStartSessionSchema, message.body) if (body.error) { @@ -328,7 +331,7 @@ export class MultisigServer { const sessionId = message.sessionId if (this.sessions.has(sessionId)) { - this.sendStratumError(client, message.id, `Duplicate sessionId: ${sessionId}`) + this.sendErrorMessage(client, message.id, `Duplicate sessionId: ${sessionId}`) return } @@ -353,7 +356,7 @@ export class MultisigServer { async handleSigningStartSessionMessage( client: MultisigServerClient, - message: StratumMessage, + message: MultisigBrokerMessage, ) { const body = await YupUtils.tryValidate(SigningStartSessionSchema, message.body) @@ -364,7 +367,7 @@ export class MultisigServer { const sessionId = message.sessionId if (this.sessions.has(sessionId)) { - this.sendStratumError(client, message.id, `Duplicate sessionId: ${sessionId}`) + this.sendErrorMessage(client, message.id, `Duplicate sessionId: ${sessionId}`) return } @@ -387,9 +390,9 @@ export class MultisigServer { client.sessionId = message.sessionId } - handleJoinSessionMessage(client: MultisigServerClient, message: StratumMessage) { + handleJoinSessionMessage(client: MultisigServerClient, message: MultisigBrokerMessage) { if (!this.sessions.has(message.sessionId)) { - this.sendStratumError(client, message.id, `Session not found: ${message.sessionId}`) + this.sendErrorMessage(client, message.id, `Session not found: ${message.sessionId}`) return } @@ -398,7 +401,7 @@ export class MultisigServer { client.sessionId = message.sessionId } - async handleIdentityMessage(client: MultisigServerClient, message: StratumMessage) { + async handleIdentityMessage(client: MultisigServerClient, message: MultisigBrokerMessage) { const body = await YupUtils.tryValidate(IdentitySchema, message.body) if (body.error) { @@ -407,7 +410,7 @@ export class MultisigServer { const session = this.sessions.get(message.sessionId) if (!session) { - this.sendStratumError(client, message.id, `Session not found: ${message.sessionId}`) + this.sendErrorMessage(client, message.id, `Session not found: ${message.sessionId}`) return } @@ -421,7 +424,7 @@ export class MultisigServer { async handleRound1PublicPackageMessage( client: MultisigServerClient, - message: StratumMessage, + message: MultisigBrokerMessage, ) { const body = await YupUtils.tryValidate(Round1PublicPackageSchema, message.body) @@ -431,12 +434,12 @@ export class MultisigServer { const session = this.sessions.get(message.sessionId) if (!session) { - this.sendStratumError(client, message.id, `Session not found: ${message.sessionId}`) + this.sendErrorMessage(client, message.id, `Session not found: ${message.sessionId}`) return } if (!isDkgSession(session)) { - this.sendStratumError( + this.sendErrorMessage( client, message.id, `Session is not a dkg session: ${message.sessionId}`, @@ -454,7 +457,7 @@ export class MultisigServer { async handleRound2PublicPackageMessage( client: MultisigServerClient, - message: StratumMessage, + message: MultisigBrokerMessage, ) { const body = await YupUtils.tryValidate(Round2PublicPackageSchema, message.body) @@ -464,12 +467,12 @@ export class MultisigServer { const session = this.sessions.get(message.sessionId) if (!session) { - this.sendStratumError(client, message.id, `Session not found: ${message.sessionId}`) + this.sendErrorMessage(client, message.id, `Session not found: ${message.sessionId}`) return } if (!isDkgSession(session)) { - this.sendStratumError( + this.sendErrorMessage( client, message.id, `Session is not a dkg session: ${message.sessionId}`, @@ -485,7 +488,10 @@ export class MultisigServer { } } - async handleDkgGetStatusMessage(client: MultisigServerClient, message: StratumMessage) { + async handleDkgGetStatusMessage( + client: MultisigServerClient, + message: MultisigBrokerMessage, + ) { const body = await YupUtils.tryValidate(DkgGetStatusSchema, message.body) if (body.error) { @@ -494,12 +500,12 @@ export class MultisigServer { const session = this.sessions.get(message.sessionId) if (!session) { - this.sendStratumError(client, message.id, `Session not found: ${message.sessionId}`) + this.sendErrorMessage(client, message.id, `Session not found: ${message.sessionId}`) return } if (!isDkgSession(session)) { - this.sendStratumError( + this.sendErrorMessage( client, message.id, `Session is not a dkg session: ${message.sessionId}`, @@ -510,7 +516,10 @@ export class MultisigServer { this.send(client.socket, 'dkg.status', message.sessionId, session.status) } - async handleSigningCommitmentMessage(client: MultisigServerClient, message: StratumMessage) { + async handleSigningCommitmentMessage( + client: MultisigServerClient, + message: MultisigBrokerMessage, + ) { const body = await YupUtils.tryValidate(SigningCommitmentSchema, message.body) if (body.error) { @@ -519,12 +528,12 @@ export class MultisigServer { const session = this.sessions.get(message.sessionId) if (!session) { - this.sendStratumError(client, message.id, `Session not found: ${message.sessionId}`) + this.sendErrorMessage(client, message.id, `Session not found: ${message.sessionId}`) return } if (!isSigningSession(session)) { - this.sendStratumError( + this.sendErrorMessage( client, message.id, `Session is not a signing session: ${message.sessionId}`, @@ -540,7 +549,10 @@ export class MultisigServer { } } - async handleSignatureShareMessage(client: MultisigServerClient, message: StratumMessage) { + async handleSignatureShareMessage( + client: MultisigServerClient, + message: MultisigBrokerMessage, + ) { const body = await YupUtils.tryValidate(SignatureShareSchema, message.body) if (body.error) { @@ -549,12 +561,12 @@ export class MultisigServer { const session = this.sessions.get(message.sessionId) if (!session) { - this.sendStratumError(client, message.id, `Session not found: ${message.sessionId}`) + this.sendErrorMessage(client, message.id, `Session not found: ${message.sessionId}`) return } if (!isSigningSession(session)) { - this.sendStratumError( + this.sendErrorMessage( client, message.id, `Session is not a signing session: ${message.sessionId}`, @@ -570,7 +582,10 @@ export class MultisigServer { } } - async handleSigningGetStatusMessage(client: MultisigServerClient, message: StratumMessage) { + async handleSigningGetStatusMessage( + client: MultisigServerClient, + message: MultisigBrokerMessage, + ) { const body = await YupUtils.tryValidate(SigningGetStatusSchema, message.body) if (body.error) { @@ -579,12 +594,12 @@ export class MultisigServer { const session = this.sessions.get(message.sessionId) if (!session) { - this.sendStratumError(client, message.id, `Session not found: ${message.sessionId}`) + this.sendErrorMessage(client, message.id, `Session not found: ${message.sessionId}`) return } if (!isSigningSession(session)) { - this.sendStratumError( + this.sendErrorMessage( client, message.id, `Session is not a signing session: ${message.sessionId}`, From 4fa53d21f6b4a0b7f5fb5094d7fe2c296b8ab5ed Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Wed, 2 Oct 2024 15:17:53 -0700 Subject: [PATCH 14/73] Split out ledger classes into files (#5469) --- ironfish-cli/src/commands/wallet/import.ts | 2 +- ironfish-cli/src/commands/wallet/mint.ts | 2 +- .../wallet/multisig/commitment/create.ts | 4 +- .../commands/wallet/multisig/dkg/create.ts | 20 +- .../commands/wallet/multisig/dkg/round1.ts | 4 +- .../commands/wallet/multisig/dkg/round2.ts | 4 +- .../commands/wallet/multisig/dkg/round3.ts | 4 +- .../commands/wallet/multisig/ledger/backup.ts | 4 +- .../commands/wallet/multisig/ledger/import.ts | 4 +- .../wallet/multisig/ledger/restore.ts | 4 +- .../wallet/multisig/participant/create.ts | 4 +- .../src/commands/wallet/multisig/sign.ts | 12 +- .../wallet/multisig/signature/create.ts | 4 +- ironfish-cli/src/commands/wallet/send.ts | 2 +- .../src/commands/wallet/transactions/sign.ts | 2 +- ironfish-cli/src/ledger/index.ts | 7 + ironfish-cli/src/ledger/ledger.ts | 113 +++++ ironfish-cli/src/ledger/ledgerMultiSigner.ts | 162 +++++++ ironfish-cli/src/ledger/ledgerSingleSigner.ts | 75 +++ ironfish-cli/src/ledger/ui.ts | 96 ++++ ironfish-cli/src/utils/ledger.ts | 431 ------------------ 21 files changed, 491 insertions(+), 469 deletions(-) create mode 100644 ironfish-cli/src/ledger/index.ts create mode 100644 ironfish-cli/src/ledger/ledger.ts create mode 100644 ironfish-cli/src/ledger/ledgerMultiSigner.ts create mode 100644 ironfish-cli/src/ledger/ledgerSingleSigner.ts create mode 100644 ironfish-cli/src/ledger/ui.ts delete mode 100644 ironfish-cli/src/utils/ledger.ts diff --git a/ironfish-cli/src/commands/wallet/import.ts b/ironfish-cli/src/commands/wallet/import.ts index 60c7c3cf5e..166653d4c8 100644 --- a/ironfish-cli/src/commands/wallet/import.ts +++ b/ironfish-cli/src/commands/wallet/import.ts @@ -5,10 +5,10 @@ import { AccountFormat, encodeAccountImport } from '@ironfish/sdk' import { Args, Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' +import { LedgerError, LedgerSingleSigner } from '../../ledger' import { checkWalletUnlocked, inputPrompt } from '../../ui' import { importFile, importPipe, longPrompt } from '../../ui/longPrompt' import { importAccount } from '../../utils' -import { LedgerError, LedgerSingleSigner } from '../../utils/ledger' export class ImportCommand extends IronfishCommand { static description = `import an account` diff --git a/ironfish-cli/src/commands/wallet/mint.ts b/ironfish-cli/src/commands/wallet/mint.ts index bcea04220d..57b4868eed 100644 --- a/ironfish-cli/src/commands/wallet/mint.ts +++ b/ironfish-cli/src/commands/wallet/mint.ts @@ -17,13 +17,13 @@ import { import { Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../command' import { IronFlag, RemoteFlags, ValueFlag } from '../../flags' +import { sendTransactionWithLedger } from '../../ledger' import * as ui from '../../ui' import { useAccount } from '../../utils' import { promptCurrency } from '../../utils/currency' import { promptExpiration } from '../../utils/expiration' import { getExplorer } from '../../utils/explorer' import { selectFee } from '../../utils/fees' -import { sendTransactionWithLedger } from '../../utils/ledger' import { watchTransaction } from '../../utils/transaction' export class Mint extends IronfishCommand { diff --git a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts index a78872b9fa..c5f680c3dc 100644 --- a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts @@ -6,8 +6,8 @@ import { RpcClient, UnsignedTransaction } from '@ironfish/sdk' import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' +import { LedgerMultiSigner } from '../../../../ledger' import * as ui from '../../../../ui' -import { LedgerDkg } from '../../../../utils/ledger' import { MultisigTransactionJson } from '../../../../utils/multisig' import { renderUnsignedTransactionDetails } from '../../../../utils/transaction' @@ -124,7 +124,7 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { unsignedTransaction: UnsignedTransaction, signers: string[], ): Promise { - const ledger = new LedgerDkg(this.logger) + const ledger = new LedgerMultiSigner(this.logger) try { await ledger.connect() } catch (e) { diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index d8ed15fa57..b4c670a08a 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -21,9 +21,9 @@ import fs from 'fs' import path from 'path' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' +import { LedgerMultiSigner } from '../../../../ledger' import { MultisigTcpClient } from '../../../../multisigBroker' import * as ui from '../../../../ui' -import { LedgerDkg } from '../../../../utils/ledger' export class DkgCreateCommand extends IronfishCommand { static description = 'Interactive command to create a multisignature account using DKG' @@ -60,10 +60,10 @@ export class DkgCreateCommand extends IronfishCommand { const client = await this.connectRpc() await ui.checkWalletUnlocked(client) - let ledger: LedgerDkg | undefined = undefined + let ledger: LedgerMultiSigner | undefined = undefined if (flags.ledger) { - ledger = new LedgerDkg(this.logger) + ledger = new LedgerMultiSigner(this.logger) try { await ledger.connect() } catch (e) { @@ -248,7 +248,7 @@ export class DkgCreateCommand extends IronfishCommand { } async getIdentityFromLedger( - ledger: LedgerDkg, + ledger: LedgerMultiSigner, client: RpcClient, name?: string, ): Promise<{ @@ -363,7 +363,7 @@ export class DkgCreateCommand extends IronfishCommand { } async performRound1WithLedger( - ledger: LedgerDkg, + ledger: LedgerMultiSigner, client: RpcClient, participantName: string, identities: string[], @@ -396,7 +396,7 @@ export class DkgCreateCommand extends IronfishCommand { currentIdentity: string, totalParticipants: number, minSigners: number, - ledger: LedgerDkg | undefined, + ledger: LedgerMultiSigner | undefined, ): Promise<{ round1: { secretPackage: string; publicPackage: string } }> { @@ -462,7 +462,7 @@ export class DkgCreateCommand extends IronfishCommand { } async performRound2WithLedger( - ledger: LedgerDkg, + ledger: LedgerMultiSigner, round1PublicPackages: string[], round1SecretPackage: string, ): Promise<{ @@ -489,7 +489,7 @@ export class DkgCreateCommand extends IronfishCommand { participantName: string, round1Result: { secretPackage: string; publicPackage: string }, totalParticipants: number, - ledger: LedgerDkg | undefined, + ledger: LedgerMultiSigner | undefined, ): Promise<{ round2: { secretPackage: string; publicPackage: string } round1PublicPackages: string[] @@ -559,7 +559,7 @@ export class DkgCreateCommand extends IronfishCommand { } async performRound3WithLedger( - ledger: LedgerDkg, + ledger: LedgerMultiSigner, client: RpcClient, accountName: string, participantName: string, @@ -676,7 +676,7 @@ export class DkgCreateCommand extends IronfishCommand { round2Result: { secretPackage: string; publicPackage: string }, round1PublicPackages: string[], totalParticipants: number, - ledger: LedgerDkg | undefined, + ledger: LedgerMultiSigner | undefined, accountCreatedAt?: number, ): Promise { let round2PublicPackages: string[] = [] diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts index 8b8c9d195c..9295f8551d 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts @@ -5,8 +5,8 @@ import { RpcClient } from '@ironfish/sdk' import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' +import { LedgerMultiSigner } from '../../../../ledger' import * as ui from '../../../../ui' -import { LedgerDkg } from '../../../../utils/ledger' export class DkgRound1Command extends IronfishCommand { static description = 'Perform round1 of the DKG protocol for multisig account creation' @@ -100,7 +100,7 @@ export class DkgRound1Command extends IronfishCommand { identities: string[], minSigners: number, ): Promise { - const ledger = new LedgerDkg(this.logger) + const ledger = new LedgerMultiSigner(this.logger) try { await ledger.connect() } catch (e) { diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts index f7f569bcf0..2cbd25909a 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts @@ -4,8 +4,8 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' +import { LedgerMultiSigner } from '../../../../ledger' import * as ui from '../../../../ui' -import { LedgerDkg } from '../../../../utils/ledger' export class DkgRound2Command extends IronfishCommand { static description = 'Perform round2 of the DKG protocol for multisig account creation' @@ -97,7 +97,7 @@ export class DkgRound2Command extends IronfishCommand { round1PublicPackages: string[], round1SecretPackage: string, ): Promise { - const ledger = new LedgerDkg(this.logger) + const ledger = new LedgerMultiSigner(this.logger) try { await ledger.connect() } catch (e) { diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts index 7d4bc8083a..9afb7cd9e6 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -14,9 +14,9 @@ import { import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' +import { LedgerMultiSigner } from '../../../../ledger' import * as ui from '../../../../ui' import { importAccount } from '../../../../utils' -import { LedgerDkg } from '../../../../utils/ledger' export class DkgRound3Command extends IronfishCommand { static description = 'Perform round3 of the DKG protocol for multisig account creation' @@ -162,7 +162,7 @@ export class DkgRound3Command extends IronfishCommand { round2SecretPackage: string, accountCreatedAt?: number, ): Promise { - const ledger = new LedgerDkg(this.logger) + const ledger = new LedgerMultiSigner(this.logger) try { await ledger.connect() } catch (e) { diff --git a/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts b/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts index a2b26435b5..f6b07fb083 100644 --- a/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts +++ b/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts @@ -2,13 +2,13 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { IronfishCommand } from '../../../../command' -import { LedgerDkg } from '../../../../utils/ledger' +import { LedgerMultiSigner } from '../../../../ledger' export class MultisigLedgerBackup extends IronfishCommand { static description = `show encrypted multisig keys from a Ledger device` async start(): Promise { - const ledger = new LedgerDkg(this.logger) + const ledger = new LedgerMultiSigner(this.logger) try { await ledger.connect() } catch (e) { diff --git a/ironfish-cli/src/commands/wallet/multisig/ledger/import.ts b/ironfish-cli/src/commands/wallet/multisig/ledger/import.ts index 1641e382c5..091589217f 100644 --- a/ironfish-cli/src/commands/wallet/multisig/ledger/import.ts +++ b/ironfish-cli/src/commands/wallet/multisig/ledger/import.ts @@ -5,9 +5,9 @@ import { ACCOUNT_SCHEMA_VERSION, AccountFormat, encodeAccountImport } from '@iro import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' +import { LedgerMultiSigner } from '../../../../ledger' import * as ui from '../../../../ui' import { importAccount } from '../../../../utils' -import { LedgerDkg } from '../../../../utils/ledger' export class MultisigLedgerImport extends IronfishCommand { static description = `import a multisig account from a Ledger device` @@ -31,7 +31,7 @@ export class MultisigLedgerImport extends IronfishCommand { const name = flags.name ?? (await ui.inputPrompt('Enter a name for the account', true)) - const ledger = new LedgerDkg(this.logger) + const ledger = new LedgerMultiSigner(this.logger) try { await ledger.connect() } catch (e) { diff --git a/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts b/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts index cad4c91e16..ad1ffa3123 100644 --- a/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts +++ b/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts @@ -3,8 +3,8 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { Args } from '@oclif/core' import { IronfishCommand } from '../../../../command' +import { LedgerMultiSigner } from '../../../../ledger' import * as ui from '../../../../ui' -import { LedgerDkg } from '../../../../utils/ledger' export class MultisigLedgerRestore extends IronfishCommand { static description = `restore encrypted multisig keys to a Ledger device` @@ -26,7 +26,7 @@ export class MultisigLedgerRestore extends IronfishCommand { ) } - const ledger = new LedgerDkg(this.logger) + const ledger = new LedgerMultiSigner(this.logger) try { await ledger.connect() } catch (e) { diff --git a/ironfish-cli/src/commands/wallet/multisig/participant/create.ts b/ironfish-cli/src/commands/wallet/multisig/participant/create.ts index 3630fcb43c..c02a4d84c5 100644 --- a/ironfish-cli/src/commands/wallet/multisig/participant/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/participant/create.ts @@ -5,8 +5,8 @@ import { RPC_ERROR_CODES, RpcRequestError } from '@ironfish/sdk' import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' +import { LedgerMultiSigner } from '../../../../ledger' import * as ui from '../../../../ui' -import { LedgerDkg } from '../../../../utils/ledger' export class MultisigIdentityCreate extends IronfishCommand { static description = `Create a multisig participant identity` @@ -71,7 +71,7 @@ export class MultisigIdentityCreate extends IronfishCommand { } async getIdentityFromLedger(): Promise { - const ledger = new LedgerDkg(this.logger) + const ledger = new LedgerMultiSigner(this.logger) try { await ledger.connect() } catch (e) { diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts index a4ddd7924b..0ee85a13b3 100644 --- a/ironfish-cli/src/commands/wallet/multisig/sign.ts +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -17,9 +17,9 @@ import { Flags, ux } from '@oclif/core' import dns from 'dns' import { IronfishCommand } from '../../../command' import { RemoteFlags } from '../../../flags' +import { LedgerMultiSigner } from '../../../ledger' import { MultisigTcpClient } from '../../../multisigBroker' import * as ui from '../../../ui' -import { LedgerDkg } from '../../../utils/ledger' import { renderUnsignedTransactionDetails, watchTransaction } from '../../../utils/transaction' // todo(patnir): this command does not differentiate between a participant and an account. @@ -62,10 +62,10 @@ export class SignMultisigTransactionCommand extends IronfishCommand { const client = await this.connectRpc() await ui.checkWalletUnlocked(client) - let ledger: LedgerDkg | undefined = undefined + let ledger: LedgerMultiSigner | undefined = undefined if (flags.ledger) { - ledger = new LedgerDkg(this.logger) + ledger = new LedgerMultiSigner(this.logger) try { await ledger.connect() } catch (e) { @@ -378,7 +378,7 @@ export class SignMultisigTransactionCommand extends IronfishCommand { identity: MultisigParticipant, signingPackageString: string, unsignedTransaction: UnsignedTransaction, - ledger: LedgerDkg | undefined, + ledger: LedgerMultiSigner | undefined, ): Promise { let signatureShare: string @@ -467,7 +467,7 @@ export class SignMultisigTransactionCommand extends IronfishCommand { participant: MultisigParticipant, totalParticipants: number, unsignedTransaction: UnsignedTransaction, - ledger: LedgerDkg | undefined, + ledger: LedgerMultiSigner | undefined, ) { let identities: string[] = [] if (!multisigClient) { @@ -534,7 +534,7 @@ export class SignMultisigTransactionCommand extends IronfishCommand { } async createSigningCommitmentWithLedger( - ledger: LedgerDkg, + ledger: LedgerMultiSigner, participant: MultisigParticipant, transactionHash: Buffer, signers: string[], diff --git a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts index db52317a67..8ef3f6076c 100644 --- a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts @@ -6,8 +6,8 @@ import { RpcClient, UnsignedTransaction } from '@ironfish/sdk' import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' +import { LedgerMultiSigner } from '../../../../ledger' import * as ui from '../../../../ui' -import { LedgerDkg } from '../../../../utils/ledger' import { MultisigTransactionJson } from '../../../../utils/multisig' import { renderUnsignedTransactionDetails } from '../../../../utils/transaction' @@ -120,7 +120,7 @@ export class CreateSignatureShareCommand extends IronfishCommand { unsignedTransaction: UnsignedTransaction, frostSigningPackage: string, ): Promise { - const ledger = new LedgerDkg(this.logger) + const ledger = new LedgerMultiSigner(this.logger) try { await ledger.connect() } catch (e) { diff --git a/ironfish-cli/src/commands/wallet/send.ts b/ironfish-cli/src/commands/wallet/send.ts index 4c7efe5231..c76da5898f 100644 --- a/ironfish-cli/src/commands/wallet/send.ts +++ b/ironfish-cli/src/commands/wallet/send.ts @@ -14,13 +14,13 @@ import { import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { HexFlag, IronFlag, RemoteFlags, ValueFlag } from '../../flags' +import { sendTransactionWithLedger } from '../../ledger' import * as ui from '../../ui' import { useAccount } from '../../utils' import { promptCurrency } from '../../utils/currency' import { promptExpiration } from '../../utils/expiration' import { getExplorer } from '../../utils/explorer' import { selectFee } from '../../utils/fees' -import { sendTransactionWithLedger } from '../../utils/ledger' import { getSpendPostTimeInMs, updateSpendPostTimeInMs } from '../../utils/spendPostTime' import { displayTransactionSummary, diff --git a/ironfish-cli/src/commands/wallet/transactions/sign.ts b/ironfish-cli/src/commands/wallet/transactions/sign.ts index 2a073e5c21..0445dc20e7 100644 --- a/ironfish-cli/src/commands/wallet/transactions/sign.ts +++ b/ironfish-cli/src/commands/wallet/transactions/sign.ts @@ -6,8 +6,8 @@ import { CurrencyUtils, RpcClient, Transaction } from '@ironfish/sdk' import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../command' import { RemoteFlags } from '../../../flags' +import { LedgerSingleSigner } from '../../../ledger' import * as ui from '../../../ui' -import { LedgerSingleSigner } from '../../../utils/ledger' import { renderTransactionDetails, watchTransaction } from '../../../utils/transaction' export class TransactionsSignCommand extends IronfishCommand { diff --git a/ironfish-cli/src/ledger/index.ts b/ironfish-cli/src/ledger/index.ts new file mode 100644 index 0000000000..4de4c815f8 --- /dev/null +++ b/ironfish-cli/src/ledger/index.ts @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +export * from './ledger' +export * from './ledgerMultiSigner' +export * from './ledgerSingleSigner' +export * from './ui' diff --git a/ironfish-cli/src/ledger/ledger.ts b/ironfish-cli/src/ledger/ledger.ts new file mode 100644 index 0000000000..b91da1c07b --- /dev/null +++ b/ironfish-cli/src/ledger/ledger.ts @@ -0,0 +1,113 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { Assert, createRootLogger, Logger } from '@ironfish/sdk' +import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' +import IronfishApp, { + KeyResponse, + ResponseAddress, + ResponseProofGenKey, + ResponseViewKey, +} from '@zondax/ledger-ironfish' +import { ResponseError } from '@zondax/ledger-js' + +export class Ledger { + app: IronfishApp | undefined + logger: Logger + PATH = "m/44'/1338'/0" + isMultisig: boolean + + constructor(isMultisig: boolean, logger?: Logger) { + this.app = undefined + this.logger = logger ? logger : createRootLogger() + this.isMultisig = isMultisig + } + + tryInstruction = async (instruction: (app: IronfishApp) => Promise) => { + await this.refreshConnection() + Assert.isNotUndefined(this.app, 'Unable to establish connection with Ledger device') + + try { + return await instruction(this.app) + } catch (error: unknown) { + if (isResponseError(error)) { + this.logger.debug(`Ledger ResponseError returnCode: ${error.returnCode.toString(16)}`) + if (error.returnCode === LedgerDeviceLockedError.returnCode) { + throw new LedgerDeviceLockedError('Please unlock your Ledger device.') + } else if (LedgerAppUnavailableError.returnCodes.includes(error.returnCode)) { + throw new LedgerAppUnavailableError() + } + + throw new LedgerError(error.errorMessage) + } + + throw error + } + } + + connect = async () => { + const transport = await TransportNodeHid.create(3000) + + transport.on('disconnect', async () => { + await transport.close() + this.app = undefined + }) + + if (transport.deviceModel) { + this.logger.debug(`${transport.deviceModel.productName} found.`) + } + + const app = new IronfishApp(transport, this.isMultisig) + + // If the app isn't open or the device is locked, this will throw an error. + await app.getVersion() + + this.app = app + + return { app, PATH: this.PATH } + } + + protected refreshConnection = async () => { + if (!this.app) { + await this.connect() + } + } +} + +export function isResponseAddress(response: KeyResponse): response is ResponseAddress { + return 'publicAddress' in response +} + +export function isResponseViewKey(response: KeyResponse): response is ResponseViewKey { + return 'viewKey' in response +} + +export function isResponseProofGenKey(response: KeyResponse): response is ResponseProofGenKey { + return 'ak' in response && 'nsk' in response +} + +export function isResponseError(error: unknown): error is ResponseError { + return 'errorMessage' in (error as object) && 'returnCode' in (error as object) +} + +export class LedgerError extends Error { + name = this.constructor.name +} + +export class LedgerDeviceLockedError extends LedgerError { + static returnCode = 0x5515 +} + +export class LedgerAppUnavailableError extends LedgerError { + static returnCodes = [ + 0x6d00, // Instruction not supported + 0xffff, // Unknown transport error + 0x6f00, // Technical error + ] + + constructor() { + super( + `Unable to connect to Ironfish app on Ledger. Please check that the device is unlocked and the app is open.`, + ) + } +} diff --git a/ironfish-cli/src/ledger/ledgerMultiSigner.ts b/ironfish-cli/src/ledger/ledgerMultiSigner.ts new file mode 100644 index 0000000000..a90c8441ed --- /dev/null +++ b/ironfish-cli/src/ledger/ledgerMultiSigner.ts @@ -0,0 +1,162 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { Logger } from '@ironfish/sdk' +import { + IronfishKeys, + KeyResponse, + ResponseDkgRound1, + ResponseDkgRound2, + ResponseIdentity, +} from '@zondax/ledger-ironfish' +import { isResponseAddress, isResponseProofGenKey, isResponseViewKey, Ledger } from './ledger' + +export class LedgerMultiSigner extends Ledger { + constructor(logger?: Logger) { + super(true, logger) + } + + dkgGetIdentity = async (index: number): Promise => { + this.logger.log('Retrieving identity from ledger device.') + + const response: ResponseIdentity = await this.tryInstruction((app) => + app.dkgGetIdentity(index, false), + ) + + return response.identity + } + + dkgRound1 = async ( + index: number, + identities: string[], + minSigners: number, + ): Promise => { + this.logger.log('Please approve the request on your ledger device.') + + return this.tryInstruction((app) => app.dkgRound1(index, identities, minSigners)) + } + + dkgRound2 = async ( + index: number, + round1PublicPackages: string[], + round1SecretPackage: string, + ): Promise => { + this.logger.log('Please approve the request on your ledger device.') + + return this.tryInstruction((app) => + app.dkgRound2(index, round1PublicPackages, round1SecretPackage), + ) + } + + dkgRound3 = async ( + index: number, + participants: string[], + round1PublicPackages: string[], + round2PublicPackages: string[], + round2SecretPackage: string, + gskBytes: string[], + ): Promise => { + this.logger.log('Please approve the request on your ledger device.') + + return this.tryInstruction((app) => + app.dkgRound3Min( + index, + participants, + round1PublicPackages, + round2PublicPackages, + round2SecretPackage, + gskBytes, + ), + ) + } + + dkgRetrieveKeys = async (): Promise<{ + publicAddress: string + viewKey: string + incomingViewKey: string + outgoingViewKey: string + proofAuthorizingKey: string + }> => { + const responseAddress: KeyResponse = await this.tryInstruction((app) => + app.dkgRetrieveKeys(IronfishKeys.PublicAddress), + ) + + if (!isResponseAddress(responseAddress)) { + throw new Error(`No public address returned.`) + } + + const responseViewKey = await this.tryInstruction((app) => + app.dkgRetrieveKeys(IronfishKeys.ViewKey), + ) + + if (!isResponseViewKey(responseViewKey)) { + throw new Error(`No view key returned.`) + } + + const responsePGK: KeyResponse = await this.tryInstruction((app) => + app.dkgRetrieveKeys(IronfishKeys.ProofGenerationKey), + ) + + if (!isResponseProofGenKey(responsePGK)) { + throw new Error(`No proof authorizing key returned.`) + } + + return { + publicAddress: responseAddress.publicAddress.toString('hex'), + viewKey: responseViewKey.viewKey.toString('hex'), + incomingViewKey: responseViewKey.ivk.toString('hex'), + outgoingViewKey: responseViewKey.ovk.toString('hex'), + proofAuthorizingKey: responsePGK.nsk.toString('hex'), + } + } + + dkgGetPublicPackage = async (): Promise => { + const response = await this.tryInstruction((app) => app.dkgGetPublicPackage()) + + return response.publicPackage + } + + reviewTransaction = async (transaction: string): Promise => { + this.logger.info( + 'Please review and approve the outputs of this transaction on your ledger device.', + ) + + const { hash } = await this.tryInstruction((app) => app.reviewTransaction(transaction)) + + return hash + } + + dkgGetCommitments = async (transactionHash: string): Promise => { + const { commitments } = await this.tryInstruction((app) => + app.dkgGetCommitments(transactionHash), + ) + + return commitments + } + + dkgSign = async ( + randomness: string, + frostSigningPackage: string, + transactionHash: string, + ): Promise => { + const { signature } = await this.tryInstruction((app) => + app.dkgSign(randomness, frostSigningPackage, transactionHash), + ) + + return signature + } + + dkgBackupKeys = async (): Promise => { + this.logger.log('Please approve the request on your ledger device.') + + const { encryptedKeys } = await this.tryInstruction((app) => app.dkgBackupKeys()) + + return encryptedKeys + } + + dkgRestoreKeys = async (encryptedKeys: string): Promise => { + this.logger.log('Please approve the request on your ledger device.') + + await this.tryInstruction((app) => app.dkgRestoreKeys(encryptedKeys)) + } +} diff --git a/ironfish-cli/src/ledger/ledgerSingleSigner.ts b/ironfish-cli/src/ledger/ledgerSingleSigner.ts new file mode 100644 index 0000000000..36c641532e --- /dev/null +++ b/ironfish-cli/src/ledger/ledgerSingleSigner.ts @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { ACCOUNT_SCHEMA_VERSION, AccountImport, Logger } from '@ironfish/sdk' +import { IronfishKeys, KeyResponse, ResponseSign } from '@zondax/ledger-ironfish' +import { isResponseAddress, isResponseProofGenKey, isResponseViewKey, Ledger } from './ledger' + +export class LedgerSingleSigner extends Ledger { + constructor(logger?: Logger) { + super(false, logger) + } + + getPublicAddress = async () => { + const response: KeyResponse = await this.tryInstruction((app) => + app.retrieveKeys(this.PATH, IronfishKeys.PublicAddress, false), + ) + + if (!isResponseAddress(response)) { + throw new Error(`No public address returned.`) + } + + return response.publicAddress.toString('hex') + } + + importAccount = async () => { + const publicAddress = await this.getPublicAddress() + + this.logger.log('Please confirm the request on your ledger device.') + + const responseViewKey: KeyResponse = await this.tryInstruction((app) => + app.retrieveKeys(this.PATH, IronfishKeys.ViewKey, true), + ) + + if (!isResponseViewKey(responseViewKey)) { + throw new Error(`No view key returned.`) + } + + const responsePGK: KeyResponse = await this.tryInstruction((app) => + app.retrieveKeys(this.PATH, IronfishKeys.ProofGenerationKey, false), + ) + + if (!isResponseProofGenKey(responsePGK)) { + throw new Error(`No proof authorizing key returned.`) + } + + const accountImport: AccountImport = { + version: ACCOUNT_SCHEMA_VERSION, + name: 'ledger', + publicAddress, + viewKey: responseViewKey.viewKey.toString('hex'), + incomingViewKey: responseViewKey.ivk.toString('hex'), + outgoingViewKey: responseViewKey.ovk.toString('hex'), + proofAuthorizingKey: responsePGK.nsk.toString('hex'), + spendingKey: null, + createdAt: null, + } + + return accountImport + } + + sign = async (message: string): Promise => { + const buffer = Buffer.from(message, 'hex') + + // max size of a transaction is 16kb + if (buffer.length > 16 * 1024) { + throw new Error('Transaction size is too large, must be less than 16kb.') + } + + const response: ResponseSign = await this.tryInstruction((app) => + app.sign(this.PATH, buffer), + ) + + return response.signature + } +} diff --git a/ironfish-cli/src/ledger/ui.ts b/ironfish-cli/src/ledger/ui.ts new file mode 100644 index 0000000000..5355ba6fcb --- /dev/null +++ b/ironfish-cli/src/ledger/ui.ts @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { + CurrencyUtils, + Logger, + RawTransaction, + RawTransactionSerde, + RpcClient, + Transaction, +} from '@ironfish/sdk' +import { Errors, ux } from '@oclif/core' +import * as ui from '../ui' +import { watchTransaction } from '../utils/transaction' +import { LedgerSingleSigner } from './ledgerSingleSigner' + +export async function sendTransactionWithLedger( + client: RpcClient, + raw: RawTransaction, + from: string | undefined, + watch: boolean, + confirm: boolean, + logger?: Logger, +): Promise { + const ledger = new LedgerSingleSigner(logger) + + try { + await ledger.connect() + } catch (e) { + if (e instanceof Error) { + Errors.error(e.message) + } else { + throw e + } + } + + const publicKey = (await client.wallet.getAccountPublicKey({ account: from })).content + .publicKey + + const ledgerPublicKey = await ledger.getPublicAddress() + + if (publicKey !== ledgerPublicKey) { + Errors.error( + `The public key on the ledger device does not match the public key of the account '${from}'`, + ) + } + + const buildTransactionResponse = await client.wallet.buildTransaction({ + account: from, + rawTransaction: RawTransactionSerde.serialize(raw).toString('hex'), + }) + + const unsignedTransaction = buildTransactionResponse.content.unsignedTransaction + + const signature = (await ledger.sign(unsignedTransaction)).toString('hex') + + ux.stdout(`\nSignature: ${signature}`) + + const addSignatureResponse = await client.wallet.addSignature({ + unsignedTransaction, + signature, + }) + + const signedTransaction = addSignatureResponse.content.transaction + const bytes = Buffer.from(signedTransaction, 'hex') + + const transaction = new Transaction(bytes) + + ux.stdout(`\nSigned Transaction: ${signedTransaction}`) + ux.stdout(`\nHash: ${transaction.hash().toString('hex')}`) + ux.stdout(`Fee: ${CurrencyUtils.render(transaction.fee(), true)}`) + + await ui.confirmOrQuit('Would you like to broadcast this transaction?', confirm) + + const addTransactionResponse = await client.wallet.addTransaction({ + transaction: signedTransaction, + broadcast: true, + }) + + if (addTransactionResponse.content.accepted === false) { + Errors.error( + `Transaction '${transaction.hash().toString('hex')}' was not accepted into the mempool`, + ) + } + + if (watch) { + ux.stdout('') + + await watchTransaction({ + client, + logger, + account: from, + hash: transaction.hash().toString('hex'), + }) + } +} diff --git a/ironfish-cli/src/utils/ledger.ts b/ironfish-cli/src/utils/ledger.ts deleted file mode 100644 index fec0fffb5b..0000000000 --- a/ironfish-cli/src/utils/ledger.ts +++ /dev/null @@ -1,431 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { - ACCOUNT_SCHEMA_VERSION, - AccountImport, - Assert, - createRootLogger, - CurrencyUtils, - Logger, - RawTransaction, - RawTransactionSerde, - RpcClient, - Transaction, -} from '@ironfish/sdk' -import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' -import { Errors, ux } from '@oclif/core' -import IronfishApp, { - IronfishKeys, - KeyResponse, - ResponseAddress, - ResponseDkgRound1, - ResponseDkgRound2, - ResponseIdentity, - ResponseProofGenKey, - ResponseSign, - ResponseViewKey, -} from '@zondax/ledger-ironfish' -import { ResponseError } from '@zondax/ledger-js' // todo: ResponseError will be exported from @zondax/ledger-ironfish in the future. Remove this line when it happens. -import * as ui from '../ui' -import { watchTransaction } from './transaction' - -class LedgerBase { - app: IronfishApp | undefined - logger: Logger - PATH = "m/44'/1338'/0" - isDkg: boolean - - constructor(isDkg: boolean, logger?: Logger) { - this.app = undefined - this.logger = logger ? logger : createRootLogger() - this.isDkg = isDkg - } - - tryInstruction = async (instruction: (app: IronfishApp) => Promise) => { - await this.refreshConnection() - Assert.isNotUndefined(this.app, 'Unable to establish connection with Ledger device') - - try { - return await instruction(this.app) - } catch (error: unknown) { - if (isResponseError(error)) { - this.logger.debug(`Ledger ResponseError returnCode: ${error.returnCode.toString(16)}`) - if (error.returnCode === LedgerDeviceLockedError.returnCode) { - throw new LedgerDeviceLockedError('Please unlock your Ledger device.') - } else if (LedgerAppUnavailableError.returnCodes.includes(error.returnCode)) { - throw new LedgerAppUnavailableError() - } - - throw new LedgerError(error.errorMessage) - } - - throw error - } - } - - connect = async () => { - const transport = await TransportNodeHid.create(3000) - - transport.on('disconnect', async () => { - await transport.close() - this.app = undefined - }) - - if (transport.deviceModel) { - this.logger.debug(`${transport.deviceModel.productName} found.`) - } - - const app = new IronfishApp(transport, this.isDkg) - - // If the app isn't open or the device is locked, this will throw an error. - await app.getVersion() - - this.app = app - - return { app, PATH: this.PATH } - } - - protected refreshConnection = async () => { - if (!this.app) { - await this.connect() - } - } -} - -export class LedgerDkg extends LedgerBase { - constructor(logger?: Logger) { - super(true, logger) - } - - dkgGetIdentity = async (index: number): Promise => { - this.logger.log('Retrieving identity from ledger device.') - - const response: ResponseIdentity = await this.tryInstruction((app) => - app.dkgGetIdentity(index, false), - ) - - return response.identity - } - - dkgRound1 = async ( - index: number, - identities: string[], - minSigners: number, - ): Promise => { - this.logger.log('Please approve the request on your ledger device.') - - return this.tryInstruction((app) => app.dkgRound1(index, identities, minSigners)) - } - - dkgRound2 = async ( - index: number, - round1PublicPackages: string[], - round1SecretPackage: string, - ): Promise => { - this.logger.log('Please approve the request on your ledger device.') - - return this.tryInstruction((app) => - app.dkgRound2(index, round1PublicPackages, round1SecretPackage), - ) - } - - dkgRound3 = async ( - index: number, - participants: string[], - round1PublicPackages: string[], - round2PublicPackages: string[], - round2SecretPackage: string, - gskBytes: string[], - ): Promise => { - this.logger.log('Please approve the request on your ledger device.') - - return this.tryInstruction((app) => - app.dkgRound3Min( - index, - participants, - round1PublicPackages, - round2PublicPackages, - round2SecretPackage, - gskBytes, - ), - ) - } - - dkgRetrieveKeys = async (): Promise<{ - publicAddress: string - viewKey: string - incomingViewKey: string - outgoingViewKey: string - proofAuthorizingKey: string - }> => { - const responseAddress: KeyResponse = await this.tryInstruction((app) => - app.dkgRetrieveKeys(IronfishKeys.PublicAddress), - ) - - if (!isResponseAddress(responseAddress)) { - throw new Error(`No public address returned.`) - } - - const responseViewKey = await this.tryInstruction((app) => - app.dkgRetrieveKeys(IronfishKeys.ViewKey), - ) - - if (!isResponseViewKey(responseViewKey)) { - throw new Error(`No view key returned.`) - } - - const responsePGK: KeyResponse = await this.tryInstruction((app) => - app.dkgRetrieveKeys(IronfishKeys.ProofGenerationKey), - ) - - if (!isResponseProofGenKey(responsePGK)) { - throw new Error(`No proof authorizing key returned.`) - } - - return { - publicAddress: responseAddress.publicAddress.toString('hex'), - viewKey: responseViewKey.viewKey.toString('hex'), - incomingViewKey: responseViewKey.ivk.toString('hex'), - outgoingViewKey: responseViewKey.ovk.toString('hex'), - proofAuthorizingKey: responsePGK.nsk.toString('hex'), - } - } - - dkgGetPublicPackage = async (): Promise => { - const response = await this.tryInstruction((app) => app.dkgGetPublicPackage()) - - return response.publicPackage - } - - reviewTransaction = async (transaction: string): Promise => { - this.logger.info( - 'Please review and approve the outputs of this transaction on your ledger device.', - ) - - const { hash } = await this.tryInstruction((app) => app.reviewTransaction(transaction)) - - return hash - } - - dkgGetCommitments = async (transactionHash: string): Promise => { - const { commitments } = await this.tryInstruction((app) => - app.dkgGetCommitments(transactionHash), - ) - - return commitments - } - - dkgSign = async ( - randomness: string, - frostSigningPackage: string, - transactionHash: string, - ): Promise => { - const { signature } = await this.tryInstruction((app) => - app.dkgSign(randomness, frostSigningPackage, transactionHash), - ) - - return signature - } - - dkgBackupKeys = async (): Promise => { - this.logger.log('Please approve the request on your ledger device.') - - const { encryptedKeys } = await this.tryInstruction((app) => app.dkgBackupKeys()) - - return encryptedKeys - } - - dkgRestoreKeys = async (encryptedKeys: string): Promise => { - this.logger.log('Please approve the request on your ledger device.') - - await this.tryInstruction((app) => app.dkgRestoreKeys(encryptedKeys)) - } -} - -export class LedgerSingleSigner extends LedgerBase { - constructor(logger?: Logger) { - super(false, logger) - } - - getPublicAddress = async () => { - const response: KeyResponse = await this.tryInstruction((app) => - app.retrieveKeys(this.PATH, IronfishKeys.PublicAddress, false), - ) - - if (!isResponseAddress(response)) { - throw new Error(`No public address returned.`) - } - - return response.publicAddress.toString('hex') - } - - importAccount = async () => { - const publicAddress = await this.getPublicAddress() - - this.logger.log('Please confirm the request on your ledger device.') - - const responseViewKey: KeyResponse = await this.tryInstruction((app) => - app.retrieveKeys(this.PATH, IronfishKeys.ViewKey, true), - ) - - if (!isResponseViewKey(responseViewKey)) { - throw new Error(`No view key returned.`) - } - - const responsePGK: KeyResponse = await this.tryInstruction((app) => - app.retrieveKeys(this.PATH, IronfishKeys.ProofGenerationKey, false), - ) - - if (!isResponseProofGenKey(responsePGK)) { - throw new Error(`No proof authorizing key returned.`) - } - - const accountImport: AccountImport = { - version: ACCOUNT_SCHEMA_VERSION, - name: 'ledger', - publicAddress, - viewKey: responseViewKey.viewKey.toString('hex'), - incomingViewKey: responseViewKey.ivk.toString('hex'), - outgoingViewKey: responseViewKey.ovk.toString('hex'), - proofAuthorizingKey: responsePGK.nsk.toString('hex'), - spendingKey: null, - createdAt: null, - } - - return accountImport - } - - sign = async (message: string): Promise => { - const buffer = Buffer.from(message, 'hex') - - // max size of a transaction is 16kb - if (buffer.length > 16 * 1024) { - throw new Error('Transaction size is too large, must be less than 16kb.') - } - - const response: ResponseSign = await this.tryInstruction((app) => - app.sign(this.PATH, buffer), - ) - - return response.signature - } -} - -function isResponseAddress(response: KeyResponse): response is ResponseAddress { - return 'publicAddress' in response -} - -function isResponseViewKey(response: KeyResponse): response is ResponseViewKey { - return 'viewKey' in response -} - -function isResponseProofGenKey(response: KeyResponse): response is ResponseProofGenKey { - return 'ak' in response && 'nsk' in response -} - -function isResponseError(error: unknown): error is ResponseError { - return 'errorMessage' in (error as object) && 'returnCode' in (error as object) -} - -export class LedgerError extends Error { - name = this.constructor.name -} - -export class LedgerDeviceLockedError extends LedgerError { - static returnCode = 0x5515 -} - -export class LedgerAppUnavailableError extends LedgerError { - static returnCodes = [ - 0x6d00, // Instruction not supported - 0xffff, // Unknown transport error - 0x6f00, // Technical error - ] - - constructor() { - super( - `Unable to connect to Ironfish app on Ledger. Please check that the device is unlocked and the app is open.`, - ) - } -} - -export async function sendTransactionWithLedger( - client: RpcClient, - raw: RawTransaction, - from: string | undefined, - watch: boolean, - confirm: boolean, - logger?: Logger, -): Promise { - const ledger = new LedgerSingleSigner(logger) - try { - await ledger.connect() - } catch (e) { - if (e instanceof Error) { - Errors.error(e.message) - } else { - throw e - } - } - - const publicKey = (await client.wallet.getAccountPublicKey({ account: from })).content - .publicKey - - const ledgerPublicKey = await ledger.getPublicAddress() - - if (publicKey !== ledgerPublicKey) { - Errors.error( - `The public key on the ledger device does not match the public key of the account '${from}'`, - ) - } - - const buildTransactionResponse = await client.wallet.buildTransaction({ - account: from, - rawTransaction: RawTransactionSerde.serialize(raw).toString('hex'), - }) - - const unsignedTransaction = buildTransactionResponse.content.unsignedTransaction - - const signature = (await ledger.sign(unsignedTransaction)).toString('hex') - - ux.stdout(`\nSignature: ${signature}`) - - const addSignatureResponse = await client.wallet.addSignature({ - unsignedTransaction, - signature, - }) - - const signedTransaction = addSignatureResponse.content.transaction - const bytes = Buffer.from(signedTransaction, 'hex') - - const transaction = new Transaction(bytes) - - ux.stdout(`\nSigned Transaction: ${signedTransaction}`) - ux.stdout(`\nHash: ${transaction.hash().toString('hex')}`) - ux.stdout(`Fee: ${CurrencyUtils.render(transaction.fee(), true)}`) - - await ui.confirmOrQuit('Would you like to broadcast this transaction?', confirm) - - const addTransactionResponse = await client.wallet.addTransaction({ - transaction: signedTransaction, - broadcast: true, - }) - - if (addTransactionResponse.content.accepted === false) { - Errors.error( - `Transaction '${transaction.hash().toString('hex')}' was not accepted into the mempool`, - ) - } - - if (watch) { - ux.stdout('') - - await watchTransaction({ - client, - logger, - account: from, - hash: transaction.hash().toString('hex'), - }) - } -} From a20f8e0005dafe69941686ca8891c3e49c066323 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Wed, 2 Oct 2024 15:46:32 -0700 Subject: [PATCH 15/73] adds TLS support to multisig server (#5465) uses TlsUtils from ironfish sdk to create TLS key and cert files on server start adds '--tls' flag to server, dkg:create, multisig:sign commands enables TLS by default extracts repeated client creation logic into MultisigServerUtils.createClient util function Closes IFL-3011 Closes IFL-3012 --- .../commands/wallet/multisig/dkg/create.ts | 34 +++++++-------- .../src/commands/wallet/multisig/server.ts | 42 ++++++++++++++++--- .../src/commands/wallet/multisig/sign.ts | 35 +++++++--------- ironfish-cli/src/multisigBroker/index.ts | 1 + ironfish-cli/src/multisigBroker/utils.ts | 30 +++++++++++++ 5 files changed, 99 insertions(+), 43 deletions(-) create mode 100644 ironfish-cli/src/multisigBroker/utils.ts diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index b4c670a08a..becc16c54b 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -11,18 +11,16 @@ import { AccountFormat, Assert, encodeAccountImport, - parseUrl, PromiseUtils, RpcClient, } from '@ironfish/sdk' import { Flags, ux } from '@oclif/core' -import dns from 'dns' import fs from 'fs' import path from 'path' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' import { LedgerMultiSigner } from '../../../../ledger' -import { MultisigTcpClient } from '../../../../multisigBroker' +import { MultisigBrokerUtils, MultisigClient } from '../../../../multisigBroker' import * as ui from '../../../../ui' export class DkgCreateCommand extends IronfishCommand { @@ -53,6 +51,12 @@ export class DkgCreateCommand extends IronfishCommand { description: 'Unique ID for a multisig server session to join', dependsOn: ['server'], }), + tls: Flags.boolean({ + description: 'connect to the multisig server over TLS', + dependsOn: ['server'], + allowNo: true, + default: true, + }), } async start(): Promise { @@ -83,18 +87,12 @@ export class DkgCreateCommand extends IronfishCommand { accountCreatedAt = statusResponse.content.blockchain.head.sequence } - let multisigClient: MultisigTcpClient | null = null + let multisigClient: MultisigClient | null = null if (flags.server) { - const parsed = parseUrl(flags.server) - - Assert.isNotNull(parsed.hostname) - Assert.isNotNull(parsed.port) - - const resolved = await dns.promises.lookup(parsed.hostname) - const host = resolved.address - const port = parsed.port - - multisigClient = new MultisigTcpClient({ host, port, logger: this.logger }) + multisigClient = await MultisigBrokerUtils.createClient(flags.server, { + tls: flags.tls, + logger: this.logger, + }) multisigClient.start() if (flags.sessionId) { @@ -304,7 +302,7 @@ export class DkgCreateCommand extends IronfishCommand { } async getDkgConfig( - multisigClient: MultisigTcpClient | null, + multisigClient: MultisigClient | null, ledger: boolean, ): Promise<{ totalParticipants: number; minSigners: number }> { if (multisigClient?.sessionId) { @@ -391,7 +389,7 @@ export class DkgCreateCommand extends IronfishCommand { async performRound1( client: RpcClient, - multisigClient: MultisigTcpClient | null, + multisigClient: MultisigClient | null, participantName: string, currentIdentity: string, totalParticipants: number, @@ -485,7 +483,7 @@ export class DkgCreateCommand extends IronfishCommand { async performRound2( client: RpcClient, - multisigClient: MultisigTcpClient | null, + multisigClient: MultisigClient | null, participantName: string, round1Result: { secretPackage: string; publicPackage: string }, totalParticipants: number, @@ -670,7 +668,7 @@ export class DkgCreateCommand extends IronfishCommand { async performRound3( client: RpcClient, - multisigClient: MultisigTcpClient | null, + multisigClient: MultisigClient | null, accountName: string, participantName: string, round2Result: { secretPackage: string; publicPackage: string }, diff --git a/ironfish-cli/src/commands/wallet/multisig/server.ts b/ironfish-cli/src/commands/wallet/multisig/server.ts index 20664e6963..2c67067822 100644 --- a/ironfish-cli/src/commands/wallet/multisig/server.ts +++ b/ironfish-cli/src/commands/wallet/multisig/server.ts @@ -2,10 +2,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { TlsUtils } from '@ironfish/sdk' import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../command' import { MultisigServer } from '../../../multisigBroker' -import { MultisigTcpAdapter } from '../../../multisigBroker/adapters' +import { + IMultisigBrokerAdapter, + MultisigTcpAdapter, + MultisigTlsAdapter, +} from '../../../multisigBroker/adapters' export class MultisigServerCommand extends IronfishCommand { static description = 'start a server to broker messages for a multisig session' @@ -19,6 +24,11 @@ export class MultisigServerCommand extends IronfishCommand { description: 'port for the multisig server', default: 9035, }), + tls: Flags.boolean({ + description: 'enable TLS on the multisig server', + allowNo: true, + default: true, + }), } async start(): Promise { @@ -26,11 +36,31 @@ export class MultisigServerCommand extends IronfishCommand { const server = new MultisigServer({ logger: this.logger }) - const adapter = new MultisigTcpAdapter({ - logger: this.logger, - host: flags.host, - port: flags.port, - }) + let adapter: IMultisigBrokerAdapter + if (flags.tls) { + const fileSystem = this.sdk.fileSystem + const nodeKeyPath = this.sdk.config.get('tlsKeyPath') + const nodeCertPath = this.sdk.config.get('tlsCertPath') + const tlsOptions = await TlsUtils.getTlsOptions( + fileSystem, + nodeKeyPath, + nodeCertPath, + this.logger, + ) + + adapter = new MultisigTlsAdapter({ + logger: this.logger, + host: flags.host, + port: flags.port, + tlsOptions, + }) + } else { + adapter = new MultisigTcpAdapter({ + logger: this.logger, + host: flags.host, + port: flags.port, + }) + } server.mount(adapter) await server.start() diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts index 0ee85a13b3..afcbd0bf23 100644 --- a/ironfish-cli/src/commands/wallet/multisig/sign.ts +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -4,21 +4,18 @@ import { multisig } from '@ironfish/rust-nodejs' import { - Assert, CurrencyUtils, Identity, - parseUrl, PromiseUtils, RpcClient, Transaction, UnsignedTransaction, } from '@ironfish/sdk' import { Flags, ux } from '@oclif/core' -import dns from 'dns' import { IronfishCommand } from '../../../command' import { RemoteFlags } from '../../../flags' import { LedgerMultiSigner } from '../../../ledger' -import { MultisigTcpClient } from '../../../multisigBroker' +import { MultisigBrokerUtils, MultisigClient } from '../../../multisigBroker' import * as ui from '../../../ui' import { renderUnsignedTransactionDetails, watchTransaction } from '../../../utils/transaction' @@ -55,6 +52,12 @@ export class SignMultisigTransactionCommand extends IronfishCommand { description: 'Unique ID for a multisig server session to join', dependsOn: ['server'], }), + tls: Flags.boolean({ + description: 'connect to the multisig server over TLS', + dependsOn: ['server'], + allowNo: true, + default: true, + }), } async start(): Promise { @@ -115,18 +118,12 @@ export class SignMultisigTransactionCommand extends IronfishCommand { ) } - let multisigClient: MultisigTcpClient | null = null + let multisigClient: MultisigClient | null = null if (flags.server) { - const parsed = parseUrl(flags.server) - - Assert.isNotNull(parsed.hostname) - Assert.isNotNull(parsed.port) - - const resolved = await dns.promises.lookup(parsed.hostname) - const host = resolved.address - const port = parsed.port - - multisigClient = new MultisigTcpClient({ host, port, logger: this.logger }) + multisigClient = await MultisigBrokerUtils.createClient(flags.server, { + tls: flags.tls, + logger: this.logger, + }) multisigClient.start() let sessionId = flags.sessionId @@ -233,7 +230,7 @@ export class SignMultisigTransactionCommand extends IronfishCommand { } async getSigningConfig( - multisigClient: MultisigTcpClient | null, + multisigClient: MultisigClient | null, unsignedTransactionFlag?: string, ): Promise<{ unsignedTransaction: UnsignedTransaction; totalParticipants: number }> { if (multisigClient?.sessionId) { @@ -288,7 +285,7 @@ export class SignMultisigTransactionCommand extends IronfishCommand { private async performAggregateSignatures( client: RpcClient, - multisigClient: MultisigTcpClient | null, + multisigClient: MultisigClient | null, accountName: string, signingPackage: string, signatureShare: string, @@ -411,7 +408,7 @@ export class SignMultisigTransactionCommand extends IronfishCommand { private async performAggregateCommitments( client: RpcClient, - multisigClient: MultisigTcpClient | null, + multisigClient: MultisigClient | null, accountName: string, commitment: string, identities: string[], @@ -462,7 +459,7 @@ export class SignMultisigTransactionCommand extends IronfishCommand { private async performCreateSigningCommitment( client: RpcClient, - multisigClient: MultisigTcpClient | null, + multisigClient: MultisigClient | null, accountName: string, participant: MultisigParticipant, totalParticipants: number, diff --git a/ironfish-cli/src/multisigBroker/index.ts b/ironfish-cli/src/multisigBroker/index.ts index 59dd82a39b..1344825732 100644 --- a/ironfish-cli/src/multisigBroker/index.ts +++ b/ironfish-cli/src/multisigBroker/index.ts @@ -4,3 +4,4 @@ export * from './clients' export { MultisigServer } from './server' +export * from './utils' diff --git a/ironfish-cli/src/multisigBroker/utils.ts b/ironfish-cli/src/multisigBroker/utils.ts new file mode 100644 index 0000000000..28f3f12a02 --- /dev/null +++ b/ironfish-cli/src/multisigBroker/utils.ts @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { Assert, Logger, parseUrl } from '@ironfish/sdk' +import dns from 'dns' +import { MultisigClient, MultisigTcpClient, MultisigTlsClient } from './clients' + +async function createClient( + serverAddress: string, + options: { tls: boolean; logger: Logger }, +): Promise { + const parsed = parseUrl(serverAddress) + + Assert.isNotNull(parsed.hostname) + Assert.isNotNull(parsed.port) + + const resolved = await dns.promises.lookup(parsed.hostname) + const host = resolved.address + const port = parsed.port + + if (options.tls) { + return new MultisigTlsClient({ host, port, logger: options.logger }) + } else { + return new MultisigTcpClient({ host, port, logger: options.logger }) + } +} + +export const MultisigBrokerUtils = { + createClient, +} From b8ebc0dbfbe972abd406d3d51dda54bf06351750 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:22:08 -0700 Subject: [PATCH 16/73] encrypts multisig server messages (#5470) uses xchacha20poly1305 to encrypt all string fields in messages sent to multisig server client decrypts string fields in messages received from multisig server derives the client key from a passphrase and the session ID (uses the bytes of the session ID, which is a UUID, for the salt and nonce). ensures that any client in the session can derive the key if they have the passphrase. adds passphrase flags and prompts to dkg:create and multisig:sign NOTE: numeric fields, like minSigners, are not currently encrypted Closes IFL-3013 --- .../commands/wallet/multisig/dkg/create.ts | 22 ++++- .../src/commands/wallet/multisig/sign.ts | 22 +++-- .../src/multisigBroker/clients/client.ts | 81 ++++++++++++++++++- .../src/multisigBroker/clients/tcpClient.ts | 4 +- ironfish-cli/src/multisigBroker/utils.ts | 16 +++- 5 files changed, 130 insertions(+), 15 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index becc16c54b..11558746f6 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -51,6 +51,10 @@ export class DkgCreateCommand extends IronfishCommand { description: 'Unique ID for a multisig server session to join', dependsOn: ['server'], }), + passphrase: Flags.string({ + description: 'Passphrase to join the multisig server session', + dependsOn: ['server'], + }), tls: Flags.boolean({ description: 'connect to the multisig server over TLS', dependsOn: ['server'], @@ -89,14 +93,28 @@ export class DkgCreateCommand extends IronfishCommand { let multisigClient: MultisigClient | null = null if (flags.server) { + let sessionId = flags.sessionId + if (!sessionId) { + sessionId = await ui.inputPrompt( + 'Enter the ID of a multisig session to join, or press enter to start a new session', + false, + ) + } + + let passphrase = flags.passphrase + if (!passphrase) { + passphrase = await ui.inputPrompt('Enter the passphrase for the multisig session', true) + } + multisigClient = await MultisigBrokerUtils.createClient(flags.server, { + passphrase, tls: flags.tls, logger: this.logger, }) multisigClient.start() - if (flags.sessionId) { - multisigClient.joinSession(flags.sessionId) + if (sessionId) { + multisigClient.joinSession(sessionId) } } diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts index afcbd0bf23..9bab2fd31c 100644 --- a/ironfish-cli/src/commands/wallet/multisig/sign.ts +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -52,6 +52,10 @@ export class SignMultisigTransactionCommand extends IronfishCommand { description: 'Unique ID for a multisig server session to join', dependsOn: ['server'], }), + passphrase: Flags.string({ + description: 'Passphrase to join the multisig server session', + dependsOn: ['server'], + }), tls: Flags.boolean({ description: 'connect to the multisig server over TLS', dependsOn: ['server'], @@ -120,12 +124,6 @@ export class SignMultisigTransactionCommand extends IronfishCommand { let multisigClient: MultisigClient | null = null if (flags.server) { - multisigClient = await MultisigBrokerUtils.createClient(flags.server, { - tls: flags.tls, - logger: this.logger, - }) - multisigClient.start() - let sessionId = flags.sessionId if (!sessionId) { sessionId = await ui.inputPrompt( @@ -134,6 +132,18 @@ export class SignMultisigTransactionCommand extends IronfishCommand { ) } + let passphrase = flags.passphrase + if (!passphrase) { + passphrase = await ui.inputPrompt('Enter the passphrase for the multisig session', true) + } + + multisigClient = await MultisigBrokerUtils.createClient(flags.server, { + passphrase, + tls: flags.tls, + logger: this.logger, + }) + multisigClient.start() + if (sessionId) { multisigClient.joinSession(sessionId) } diff --git a/ironfish-cli/src/multisigBroker/clients/client.ts b/ironfish-cli/src/multisigBroker/clients/client.ts index 1395d11d89..2e26870bff 100644 --- a/ironfish-cli/src/multisigBroker/clients/client.ts +++ b/ironfish-cli/src/multisigBroker/clients/client.ts @@ -1,6 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { xchacha20poly1305 } from '@ironfish/rust-nodejs' import { ErrorUtils, Event, @@ -62,8 +63,9 @@ export abstract class MultisigClient { readonly onMultisigBrokerError = new Event<[MultisigBrokerMessageWithError]>() sessionId: string | null = null + passphrase: string - constructor(options: { logger: Logger }) { + constructor(options: { passphrase: string; logger: Logger }) { this.logger = options.logger this.version = 3 @@ -72,6 +74,20 @@ export abstract class MultisigClient { this.connected = false this.connectWarned = false this.connectTimeout = null + + this.passphrase = options.passphrase + } + + get key(): xchacha20poly1305.XChaCha20Poly1305Key { + if (!this.sessionId) { + throw new Error('Client must join a session before encrypting/decrypting messages') + } + + const sessionIdBytes = Buffer.from(this.sessionId) + const salt = sessionIdBytes.subarray(0, 32) + const nonce = sessionIdBytes.subarray(sessionIdBytes.length - 24) + + return xchacha20poly1305.XChaCha20Poly1305Key.fromParts(this.passphrase, salt, nonce) } protected abstract connect(): Promise @@ -200,7 +216,7 @@ export abstract class MultisigClient { id: this.nextMessageId++, method, sessionId: this.sessionId, - body, + body: this.encryptMessageBody(body), } this.writeData(JSON.stringify(message) + '\n') @@ -252,6 +268,9 @@ export abstract class MultisigClient { this.logger.debug(`Server sent ${header.result.method} message`) + // Decrypt fields in the message body + header.result.body = this.decryptMessageBody(header.result.body) + switch (header.result.method) { case 'identity': { const body = await YupUtils.tryValidate(IdentitySchema, header.result.body) @@ -329,4 +348,62 @@ export abstract class MultisigClient { } } } + + private encryptMessageBody(body: unknown): object { + let encrypted = body as object + for (const [key, value] of Object.entries(body as object)) { + if (typeof value === 'string') { + encrypted = { + ...encrypted, + [key]: this.key.encrypt(Buffer.from(value)).toString('hex'), + } + } else if (value instanceof Array) { + const encryptedItems = [] + for (const item of value) { + if (typeof item === 'string') { + encryptedItems.push(this.key.encrypt(Buffer.from(item)).toString('hex')) + } else { + encryptedItems.push(item) + } + } + encrypted = { + ...encrypted, + [key]: encryptedItems, + } + } + } + + return encrypted + } + + private decryptMessageBody(body?: unknown): object | undefined { + if (!body) { + return + } + + let decrypted = body as object + for (const [key, value] of Object.entries(body as object)) { + if (typeof value === 'string') { + decrypted = { + ...decrypted, + [key]: this.key.decrypt(Buffer.from(value, 'hex')).toString(), + } + } else if (value instanceof Array) { + const decryptedItems = [] + for (const item of value) { + if (typeof item === 'string') { + decryptedItems.push(this.key.decrypt(Buffer.from(item, 'hex')).toString()) + } else { + decryptedItems.push(item) + } + } + decrypted = { + ...decrypted, + [key]: decryptedItems, + } + } + } + + return decrypted + } } diff --git a/ironfish-cli/src/multisigBroker/clients/tcpClient.ts b/ironfish-cli/src/multisigBroker/clients/tcpClient.ts index 4465d3ba14..18e74155b5 100644 --- a/ironfish-cli/src/multisigBroker/clients/tcpClient.ts +++ b/ironfish-cli/src/multisigBroker/clients/tcpClient.ts @@ -11,8 +11,8 @@ export class MultisigTcpClient extends MultisigClient { client: net.Socket | null = null - constructor(options: { host: string; port: number; logger: Logger }) { - super({ logger: options.logger }) + constructor(options: { host: string; port: number; passphrase: string; logger: Logger }) { + super({ passphrase: options.passphrase, logger: options.logger }) this.host = options.host this.port = options.port } diff --git a/ironfish-cli/src/multisigBroker/utils.ts b/ironfish-cli/src/multisigBroker/utils.ts index 28f3f12a02..1c4bd0435e 100644 --- a/ironfish-cli/src/multisigBroker/utils.ts +++ b/ironfish-cli/src/multisigBroker/utils.ts @@ -7,7 +7,7 @@ import { MultisigClient, MultisigTcpClient, MultisigTlsClient } from './clients' async function createClient( serverAddress: string, - options: { tls: boolean; logger: Logger }, + options: { passphrase: string; tls: boolean; logger: Logger }, ): Promise { const parsed = parseUrl(serverAddress) @@ -19,9 +19,19 @@ async function createClient( const port = parsed.port if (options.tls) { - return new MultisigTlsClient({ host, port, logger: options.logger }) + return new MultisigTlsClient({ + host, + port, + passphrase: options.passphrase, + logger: options.logger, + }) } else { - return new MultisigTcpClient({ host, port, logger: options.logger }) + return new MultisigTcpClient({ + host, + port, + passphrase: options.passphrase, + logger: options.logger, + }) } } From 0cc3ec351360804fb633abaa82fa22e4bd744049 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Wed, 2 Oct 2024 16:43:36 -0700 Subject: [PATCH 17/73] Render error in ui/retry (#5471) --- ironfish-cli/src/ui/retry.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ironfish-cli/src/ui/retry.ts b/ironfish-cli/src/ui/retry.ts index da206c5fb8..c016469515 100644 --- a/ironfish-cli/src/ui/retry.ts +++ b/ironfish-cli/src/ui/retry.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Logger } from '@ironfish/sdk' +import { ErrorUtils, Logger } from '@ironfish/sdk' import { confirmPrompt } from './prompt' export async function retryStep( @@ -17,7 +17,8 @@ export async function retryStep( const result = await stepFunction() return result } catch (error) { - logger.log(`An Error Occurred: ${(error as Error).message}`) + logger.log(`An Error Occurred: ${ErrorUtils.renderError(error)}`) + if (askToRetry) { const continueResponse = await confirmPrompt('Do you want to retry this step?') if (!continueResponse) { From 650a3831591c166de4635d4d46e307c0c86a4d5b Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Wed, 2 Oct 2024 16:43:50 -0700 Subject: [PATCH 18/73] Prefer the account name as the participant name (#5472) --- .../commands/wallet/multisig/dkg/create.ts | 42 +++++-------------- 1 file changed, 10 insertions(+), 32 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index 11558746f6..ca5496e378 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -83,7 +83,7 @@ export class DkgCreateCommand extends IronfishCommand { } } - const accountName = await this.getAccountName(client, flags.name) + const accountName = await this.getAccountName(client, flags.name ?? flags.participant) let accountCreatedAt = flags.createdAt if (!accountCreatedAt) { @@ -122,12 +122,12 @@ export class DkgCreateCommand extends IronfishCommand { ? await ui.retryStep( () => { Assert.isNotUndefined(ledger) - return this.getIdentityFromLedger(ledger, client, flags.participant) + return this.getIdentityFromLedger(ledger, client, accountName) }, this.logger, true, ) - : await this.getParticipant(client, flags.participant) + : await this.getParticipant(client, accountName) this.log(`Identity for ${participantName}: \n${identity} \n`) @@ -208,38 +208,16 @@ export class DkgCreateCommand extends IronfishCommand { multisigClient?.stop() } - private async getParticipant(client: RpcClient, participantName?: string) { - const identities = (await client.wallet.multisig.getIdentities()).content.identities - - if (participantName) { - const foundIdentity = identities.find((i) => i.name === participantName) - if (!foundIdentity) { - throw new Error(`Participant with name ${participantName} not found`) - } - - return { - name: foundIdentity.name, - identity: foundIdentity.identity, - } - } - - const name = await ui.inputPrompt('Enter the name of the participant', true) - const foundIdentity = identities.find((i) => i.name === name) + private async getParticipant(client: RpcClient, name: string) { + const identities = await client.wallet.multisig.getIdentities() + const foundIdentity = identities.content.identities.find((i) => i.name === name) if (foundIdentity) { - this.log('Found an identity with the same name') - - return { - ...foundIdentity, - } + return foundIdentity } - const identity = (await client.wallet.multisig.createParticipant({ name })).content.identity - - return { - name, - identity, - } + const created = await client.wallet.multisig.createParticipant({ name }) + return { name, identity: created.content.identity } } private async getAccountName(client: RpcClient, name?: string) { @@ -266,7 +244,7 @@ export class DkgCreateCommand extends IronfishCommand { async getIdentityFromLedger( ledger: LedgerMultiSigner, client: RpcClient, - name?: string, + name: string, ): Promise<{ name: string identity: string From 06b67d6786de87f454ac6b891c554f2abd9a3869 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Wed, 2 Oct 2024 23:58:17 -0700 Subject: [PATCH 19/73] fixes default tls flag in multisig commands (#5475) updates 'dkg:create' and 'multisig:sign' not to set the '--tls' flag by default. since this flag depends on the '--server' flag this means that the '--server' flag must also be provided by default --- ironfish-cli/src/commands/wallet/multisig/dkg/create.ts | 3 +-- ironfish-cli/src/commands/wallet/multisig/sign.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index ca5496e378..3d665ddcab 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -59,7 +59,6 @@ export class DkgCreateCommand extends IronfishCommand { description: 'connect to the multisig server over TLS', dependsOn: ['server'], allowNo: true, - default: true, }), } @@ -108,7 +107,7 @@ export class DkgCreateCommand extends IronfishCommand { multisigClient = await MultisigBrokerUtils.createClient(flags.server, { passphrase, - tls: flags.tls, + tls: flags.tls ?? true, logger: this.logger, }) multisigClient.start() diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts index 9bab2fd31c..f792b19df7 100644 --- a/ironfish-cli/src/commands/wallet/multisig/sign.ts +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -60,7 +60,6 @@ export class SignMultisigTransactionCommand extends IronfishCommand { description: 'connect to the multisig server over TLS', dependsOn: ['server'], allowNo: true, - default: true, }), } @@ -139,7 +138,7 @@ export class SignMultisigTransactionCommand extends IronfishCommand { multisigClient = await MultisigBrokerUtils.createClient(flags.server, { passphrase, - tls: flags.tls, + tls: flags.tls ?? true, logger: this.logger, }) multisigClient.start() From cd90e548c1b33189ce0c432864a47d8f9fe7ea55 Mon Sep 17 00:00:00 2001 From: Mat Date: Thu, 3 Oct 2024 11:18:08 -0700 Subject: [PATCH 20/73] Add a connected response from the broker server --- .../commands/wallet/multisig/dkg/create.ts | 10 ++++--- .../src/commands/wallet/multisig/sign.ts | 11 +++++--- .../src/multisigBroker/clients/client.ts | 13 ++++++++++ ironfish-cli/src/multisigBroker/messages.ts | 7 +++++ ironfish-cli/src/multisigBroker/server.ts | 26 +++++++++++++++++++ 5 files changed, 61 insertions(+), 6 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index 3d665ddcab..b4fae69951 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -112,9 +112,13 @@ export class DkgCreateCommand extends IronfishCommand { }) multisigClient.start() - if (sessionId) { - multisigClient.joinSession(sessionId) - } + multisigClient.onConnectedMessage.on(() => { + if (sessionId) { + Assert.isNotNull(multisigClient) + multisigClient.joinSession(sessionId) + multisigClient.onConnectedMessage.clear() + } + }) } const { name: participantName, identity } = ledger diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts index f792b19df7..94f12152d3 100644 --- a/ironfish-cli/src/commands/wallet/multisig/sign.ts +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -4,6 +4,7 @@ import { multisig } from '@ironfish/rust-nodejs' import { + Assert, CurrencyUtils, Identity, PromiseUtils, @@ -143,9 +144,13 @@ export class SignMultisigTransactionCommand extends IronfishCommand { }) multisigClient.start() - if (sessionId) { - multisigClient.joinSession(sessionId) - } + multisigClient.onConnectedMessage.on(() => { + if (sessionId) { + Assert.isNotNull(multisigClient) + multisigClient.joinSession(sessionId) + multisigClient.onConnectedMessage.clear() + } + }) } const { unsignedTransaction, totalParticipants } = await this.getSigningConfig( diff --git a/ironfish-cli/src/multisigBroker/clients/client.ts b/ironfish-cli/src/multisigBroker/clients/client.ts index 2e26870bff..07b030e4de 100644 --- a/ironfish-cli/src/multisigBroker/clients/client.ts +++ b/ironfish-cli/src/multisigBroker/clients/client.ts @@ -13,6 +13,8 @@ import { import { v4 as uuid } from 'uuid' import { ServerMessageMalformedError } from '../errors' import { + ConnectedMessage, + ConnectedMessageSchema, DkgGetStatusMessage, DkgStartSessionMessage, DkgStatusMessage, @@ -60,6 +62,7 @@ export abstract class MultisigClient { readonly onSigningCommitment = new Event<[SigningCommitmentMessage]>() readonly onSignatureShare = new Event<[SignatureShareMessage]>() readonly onSigningStatus = new Event<[SigningStatusMessage]>() + readonly onConnectedMessage = new Event<[ConnectedMessage]>() readonly onMultisigBrokerError = new Event<[MultisigBrokerMessageWithError]>() sessionId: string | null = null @@ -342,6 +345,16 @@ export abstract class MultisigClient { this.onSigningStatus.emit(body.result) break } + case 'connected': { + const body = await YupUtils.tryValidate(ConnectedMessageSchema, header.result.body) + + if (body.error) { + throw new ServerMessageMalformedError(body.error, header.result.method) + } + + this.onConnectedMessage.emit(body.result) + break + } default: throw new ServerMessageMalformedError(`Invalid message ${header.result.method}`) diff --git a/ironfish-cli/src/multisigBroker/messages.ts b/ironfish-cli/src/multisigBroker/messages.ts index d0c19bd296..bcdab7997b 100644 --- a/ironfish-cli/src/multisigBroker/messages.ts +++ b/ironfish-cli/src/multisigBroker/messages.ts @@ -70,6 +70,8 @@ export type SigningStatusMessage = { signatureShares: string[] } +export type ConnectedMessage = object | undefined + export const MultisigBrokerMessageSchema: yup.ObjectSchema = yup .object({ id: yup.number().required(), @@ -162,3 +164,8 @@ export const SigningStatusSchema: yup.ObjectSchema = yup signatureShares: yup.array(yup.string().defined()).defined(), }) .defined() + +export const ConnectedMessageSchema: yup.ObjectSchema = yup + .object({}) + .notRequired() + .default(undefined) diff --git a/ironfish-cli/src/multisigBroker/server.ts b/ironfish-cli/src/multisigBroker/server.ts index bb38f79f97..ba2b0dee68 100644 --- a/ironfish-cli/src/multisigBroker/server.ts +++ b/ironfish-cli/src/multisigBroker/server.ts @@ -6,6 +6,7 @@ import net from 'net' import { IMultisigBrokerAdapter } from './adapters' import { ClientMessageMalformedError } from './errors' import { + ConnectedMessage, DkgGetStatusSchema, DkgStartSessionSchema, DkgStatusMessage, @@ -143,6 +144,8 @@ export class MultisigServer { socket.on('close', () => this.onDisconnect(client)) socket.on('error', (e) => this.onError(client, e)) + this.send(socket, 'connected', '0', {}) + this.logger.debug(`Client ${client.id} connected: ${client.remoteAddress}`) this.clients.set(client.id, client) } @@ -154,6 +157,10 @@ export class MultisigServer { client.close() client.socket.removeAllListeners('close') client.socket.removeAllListeners('error') + + if (client.sessionId && !this.isSessionActive(client.sessionId)) { + this.cleanupSession(client.sessionId) + } } private async onData(client: MultisigServerClient, data: Buffer): Promise { @@ -223,6 +230,24 @@ export class MultisigServer { this.clients.delete(client.id) } + /** + * If a client has the given session ID and is connected, the associated + * session should still be considered active + */ + private isSessionActive(sessionId: string): boolean { + for (const client of this.clients.values()) { + if (client.connected && client.sessionId && client.sessionId === sessionId) { + return true + } + } + return false + } + + private cleanupSession(sessionId: string): void { + this.sessions.delete(sessionId) + this.logger.debug(`Session ${sessionId} cleaned up. Active sessions: ${this.sessions.size}`) + } + private broadcast(method: 'identity', sessionId: string, body: IdentityMessage): void private broadcast( method: 'dkg.round1', @@ -294,6 +319,7 @@ export class MultisigServer { sessionId: string, body: SigningStatusMessage, ): void + send(socket: net.Socket, method: 'connected', sessionId: string, body: ConnectedMessage): void send(socket: net.Socket, method: string, sessionId: string, body?: unknown): void { const message: MultisigBrokerMessage = { id: this.nextMessageId++, From 0a778c3a4b62202e343580d30e5a47f514a0cce7 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Thu, 3 Oct 2024 11:45:29 -0700 Subject: [PATCH 21/73] reduces log output when using multisig broker (#5481) - changes log messages to debug for packages that don't need to be sent to other participants: signing package, encrypted secret packages - does not log identities, packages, commitments, shares when using multisig broker - logs session ID on separate line when starting session Closes IFL-3014 Closes IFL-3015 Closes IFL-3019 --- .../commands/wallet/multisig/dkg/create.ts | 43 ++++++++------- .../src/commands/wallet/multisig/sign.ts | 55 ++++++++++--------- 2 files changed, 50 insertions(+), 48 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index 3d665ddcab..1d326fa962 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -128,8 +128,6 @@ export class DkgCreateCommand extends IronfishCommand { ) : await this.getParticipant(client, accountName) - this.log(`Identity for ${participantName}: \n${identity} \n`) - const { totalParticipants, minSigners } = await ui.retryStep( async () => { return this.getDkgConfig(multisigClient, !!ledger) @@ -154,14 +152,6 @@ export class DkgCreateCommand extends IronfishCommand { true, ) - this.log('\n============================================') - this.log('\nRound 1 Encrypted Secret Package:') - this.log(round1.secretPackage) - - this.log('\nRound 1 Public Package:') - this.log(round1.publicPackage) - this.log('\n============================================') - const { round2: round2Result, round1PublicPackages } = await ui.retryStep( async () => { return this.performRound2( @@ -177,14 +167,6 @@ export class DkgCreateCommand extends IronfishCommand { true, ) - this.log('\n============================================') - this.log('\nRound 2 Encrypted Secret Package:') - this.log(round2Result.secretPackage) - - this.log('\nRound 2 Public Package:') - this.log(round2Result.publicPackage) - this.log('\n============================================') - await ui.retryStep( async () => { return this.performRound3( @@ -309,10 +291,10 @@ export class DkgCreateCommand extends IronfishCommand { minSigners = message.minSigners waiting = false }) - multisigClient.getDkgStatus() ux.action.start('Waiting for signer config from server') while (waiting) { + multisigClient.getDkgStatus() await PromiseUtils.sleep(3000) } multisigClient.onDkgStatus.clear() @@ -349,7 +331,8 @@ export class DkgCreateCommand extends IronfishCommand { if (multisigClient) { multisigClient.startDkgSession(totalParticipants, minSigners) - this.log(`Started new DKG server session with ID ${multisigClient.sessionId}`) + this.log('\nStarted new DKG session:') + this.log(`${multisigClient.sessionId}`) } return { totalParticipants, minSigners } @@ -397,6 +380,8 @@ export class DkgCreateCommand extends IronfishCommand { let identities: string[] = [] if (!multisigClient) { + this.log(`Identity for ${participantName}: \n${currentIdentity} \n`) + this.log( `\nEnter ${ totalParticipants - 1 @@ -489,6 +474,14 @@ export class DkgCreateCommand extends IronfishCommand { }> { let round1PublicPackages: string[] = [] if (!multisigClient) { + this.log('\n============================================') + this.debug('\nRound 1 Encrypted Secret Package:') + this.debug(round1Result.secretPackage) + + this.log('\nRound 1 Public Package:') + this.log(round1Result.publicPackage) + this.log('\n============================================') + this.log('\nShare your Round 1 Public Package with other participants.') this.log(`\nEnter ${totalParticipants - 1} Round 1 Public Packages (excluding yours) `) @@ -632,7 +625,6 @@ export class DkgCreateCommand extends IronfishCommand { this.log( `Account ${response.content.name} imported with public address: ${dkgKeys.publicAddress}`, ) - this.log() this.log('Creating an encrypted backup of multisig keys from your Ledger device...') this.log() @@ -674,6 +666,14 @@ export class DkgCreateCommand extends IronfishCommand { ): Promise { let round2PublicPackages: string[] = [] if (!multisigClient) { + this.log('\n============================================') + this.debug('\nRound 2 Encrypted Secret Package:') + this.debug(round2Result.secretPackage) + + this.log('\nRound 2 Public Package:') + this.log(round2Result.publicPackage) + this.log('\n============================================') + this.log('\nShare your Round 2 Public Package with other participants.') this.log(`\nEnter ${totalParticipants - 1} Round 2 Public Packages (excluding yours) `) @@ -729,6 +729,7 @@ export class DkgCreateCommand extends IronfishCommand { round2PublicPackages, }) + this.log() this.log(`Account Name: ${response.content.name}`) this.log(`Public Address: ${response.content.publicAddress}`) } diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts index f792b19df7..1deefd8139 100644 --- a/ironfish-cli/src/commands/wallet/multisig/sign.ts +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -153,13 +153,6 @@ export class SignMultisigTransactionCommand extends IronfishCommand { flags.unsignedTransaction, ) - await renderUnsignedTransactionDetails( - client, - unsignedTransaction, - multisigAccountName, - this.logger, - ) - const { commitment, identities } = await ui.retryStep( async () => { return this.performCreateSigningCommitment( @@ -176,13 +169,6 @@ export class SignMultisigTransactionCommand extends IronfishCommand { true, ) - this.log('\n============================================') - this.log('\nCommitment:') - this.log(commitment) - this.log('\n============================================') - - this.log('\nShare your commitment with other participants.') - const signingPackage = await ui.retryStep(() => { return this.performAggregateCommitments( client, @@ -195,11 +181,6 @@ export class SignMultisigTransactionCommand extends IronfishCommand { ) }, this.logger) - this.log('\n============================================') - this.log('\nSigning Package:') - this.log(signingPackage) - this.log('\n============================================') - const signatureShare = await ui.retryStep( () => this.performCreateSignatureShare( @@ -214,13 +195,6 @@ export class SignMultisigTransactionCommand extends IronfishCommand { true, ) - this.log('\n============================================') - this.log('\nSignature Share:') - this.log(signatureShare) - this.log('\n============================================') - - this.log('\nShare your signature share with other participants.') - await ui.retryStep( () => this.performAggregateSignatures( @@ -286,7 +260,8 @@ export class SignMultisigTransactionCommand extends IronfishCommand { if (multisigClient) { multisigClient.startSigningSession(totalParticipants, unsignedTransactionInput) - this.log(`Started new signing session with ID ${multisigClient.sessionId}`) + this.log('\nStarted new signing session:') + this.log(`${multisigClient.sessionId}`) } return { unsignedTransaction, totalParticipants } @@ -302,6 +277,13 @@ export class SignMultisigTransactionCommand extends IronfishCommand { ): Promise { let signatureShares: string[] = [] if (!multisigClient) { + this.log('\n============================================') + this.log('\nSignature Share:') + this.log(signatureShare) + this.log('\n============================================') + + this.log('\nShare your signature share with other participants.') + this.log( `Enter ${ totalParticipants - 1 @@ -386,6 +368,11 @@ export class SignMultisigTransactionCommand extends IronfishCommand { unsignedTransaction: UnsignedTransaction, ledger: LedgerMultiSigner | undefined, ): Promise { + this.debug('\n============================================') + this.debug('\nSigning Package:') + this.debug(signingPackageString) + this.debug('\n============================================') + let signatureShare: string const signingPackage = new multisig.SigningPackage(Buffer.from(signingPackageString, 'hex')) @@ -426,6 +413,13 @@ export class SignMultisigTransactionCommand extends IronfishCommand { ) { let commitments: string[] = [] if (!multisigClient) { + this.log('\n============================================') + this.log('\nCommitment:') + this.log(commitment) + this.log('\n============================================') + + this.log('\nShare your commitment with other participants.') + this.log( `Enter ${identities.length - 1} commitments of the participants (excluding your own)`, ) @@ -513,6 +507,13 @@ export class SignMultisigTransactionCommand extends IronfishCommand { const unsignedTransactionHex = unsignedTransaction.serialize().toString('hex') + await renderUnsignedTransactionDetails( + client, + unsignedTransaction, + accountName, + this.logger, + ) + let commitment if (ledger) { await ledger.reviewTransaction(unsignedTransactionHex) From ab44674ba041cc44db56684114761dea88ccc6db Mon Sep 17 00:00:00 2001 From: Mat Date: Thu, 3 Oct 2024 12:06:00 -0700 Subject: [PATCH 22/73] Wait for connection confirmation before joining session --- .../commands/wallet/multisig/dkg/create.ts | 19 ++++++++++++++----- .../src/commands/wallet/multisig/sign.ts | 19 ++++++++++++++----- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index b4fae69951..4b63b30e0e 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -112,13 +112,22 @@ export class DkgCreateCommand extends IronfishCommand { }) multisigClient.start() + let connectionConfirmed = false + multisigClient.onConnectedMessage.on(() => { - if (sessionId) { - Assert.isNotNull(multisigClient) - multisigClient.joinSession(sessionId) - multisigClient.onConnectedMessage.clear() - } + connectionConfirmed = true + Assert.isNotNull(multisigClient) + multisigClient.onConnectedMessage.clear() }) + + if (sessionId) { + while (!connectionConfirmed) { + await PromiseUtils.sleep(500) + continue + } + + multisigClient.joinSession(sessionId) + } } const { name: participantName, identity } = ledger diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts index 94f12152d3..aad44e75a8 100644 --- a/ironfish-cli/src/commands/wallet/multisig/sign.ts +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -144,13 +144,22 @@ export class SignMultisigTransactionCommand extends IronfishCommand { }) multisigClient.start() + let connectionConfirmed = false + multisigClient.onConnectedMessage.on(() => { - if (sessionId) { - Assert.isNotNull(multisigClient) - multisigClient.joinSession(sessionId) - multisigClient.onConnectedMessage.clear() - } + connectionConfirmed = true + Assert.isNotNull(multisigClient) + multisigClient.onConnectedMessage.clear() }) + + if (sessionId) { + while (!connectionConfirmed) { + await PromiseUtils.sleep(500) + continue + } + + multisigClient.joinSession(sessionId) + } } const { unsignedTransaction, totalParticipants } = await this.getSigningConfig( From ca8b3ce9d237d2c7a6030b80d33c163d6330d622 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Thu, 3 Oct 2024 12:40:03 -0700 Subject: [PATCH 23/73] Rahul/add ledger to chainport send (#5479) * Adds unsignedTransaction flag to chainport send Allows for signing this transaction with a ledger device or other another signing method. * adds ledger flag to chainport send command You can now bridge with a ledger device in the CLI --- .../src/commands/wallet/chainport/send.ts | 49 +++++++++++++++++-- ironfish-cli/src/ledger/ui.ts | 2 + 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/chainport/send.ts b/ironfish-cli/src/commands/wallet/chainport/send.ts index 210386ab7f..51c89f3c35 100644 --- a/ironfish-cli/src/commands/wallet/chainport/send.ts +++ b/ironfish-cli/src/commands/wallet/chainport/send.ts @@ -17,6 +17,7 @@ import { Flags, ux } from '@oclif/core' import inquirer from 'inquirer' import { IronfishCommand } from '../../../command' import { HexFlag, IronFlag, RemoteFlags, ValueFlag } from '../../../flags' +import { sendTransactionWithLedger } from '../../../ledger' import * as ui from '../../../ui' import { ChainportBridgeTransaction, @@ -28,6 +29,7 @@ import { } from '../../../utils/chainport' import { isEthereumAddress } from '../../../utils/chainport/address' import { promptCurrency } from '../../../utils/currency' +import { promptExpiration } from '../../../utils/expiration' import { getExplorer } from '../../../utils/explorer' import { selectFee } from '../../../utils/fees' import { watchTransaction } from '../../../utils/transaction' @@ -74,6 +76,15 @@ export class BridgeCommand extends IronfishCommand { default: false, description: 'Allow offline transaction creation', }), + unsignedTransaction: Flags.boolean({ + default: false, + description: + 'Return a serialized UnsignedTransaction. Use it to create a transaction and build proofs but not post to the network', + }), + ledger: Flags.boolean({ + default: false, + description: 'Send a transaction using a Ledger device', + }), } async start(): Promise { @@ -98,7 +109,7 @@ export class BridgeCommand extends IronfishCommand { } } - const { targetNetwork, from, to, amount, asset, assetData } = + const { targetNetwork, from, to, amount, asset, assetData, expiration } = await this.getAndValidateInputs(client, networkId) const rawTransaction = await this.constructBridgeTransaction( @@ -110,8 +121,31 @@ export class BridgeCommand extends IronfishCommand { amount, asset, assetData, + expiration, ) + if (flags.unsignedTransaction) { + const response = await client.wallet.buildTransaction({ + account: from, + rawTransaction: RawTransactionSerde.serialize(rawTransaction).toString('hex'), + }) + this.log('Unsigned Bridge Transaction') + this.log(response.content.unsignedTransaction) + this.exit(0) + } + + if (flags.ledger) { + await sendTransactionWithLedger( + client, + rawTransaction, + from, + flags.watch, + true, + this.logger, + ) + this.exit(0) + } + await ui.confirmOrQuit() const postTransaction = await client.wallet.postTransaction({ @@ -178,7 +212,13 @@ export class BridgeCommand extends IronfishCommand { this.error('Invalid to ethereum address') } - if (flags.expiration !== undefined && flags.expiration < 0) { + let expiration = flags.expiration + + if (flags.unsignedTransaction && expiration === undefined) { + expiration = await promptExpiration({ logger: this.logger, client: client }) + } + + if (expiration !== undefined && expiration < 0) { this.error('Expiration sequence must be non-negative') } @@ -268,7 +308,7 @@ export class BridgeCommand extends IronfishCommand { }, }) } - return { targetNetwork, from, to, amount, asset, assetData } + return { targetNetwork, from, to, amount, asset, assetData, expiration } } private async constructBridgeTransaction( @@ -280,6 +320,7 @@ export class BridgeCommand extends IronfishCommand { amount: bigint, asset: ChainportToken, assetData: RpcAsset, + expiration: number | undefined, ) { const { flags } = await this.parse(BridgeCommand) @@ -310,7 +351,7 @@ export class BridgeCommand extends IronfishCommand { ], fee: flags.fee ? CurrencyUtils.encode(flags.fee) : null, feeRate: flags.feeRate ? CurrencyUtils.encode(flags.feeRate) : null, - expiration: flags.expiration, + expiration, } let rawTransaction: RawTransaction diff --git a/ironfish-cli/src/ledger/ui.ts b/ironfish-cli/src/ledger/ui.ts index 5355ba6fcb..c37567877b 100644 --- a/ironfish-cli/src/ledger/ui.ts +++ b/ironfish-cli/src/ledger/ui.ts @@ -52,6 +52,8 @@ export async function sendTransactionWithLedger( const unsignedTransaction = buildTransactionResponse.content.unsignedTransaction + ux.stdout('Please confirm the transaction on your Ledger device') + const signature = (await ledger.sign(unsignedTransaction)).toString('hex') ux.stdout(`\nSignature: ${signature}`) From a92a85945ac7ae4c662247dd8e881432dfc7035d Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Thu, 3 Oct 2024 13:24:13 -0700 Subject: [PATCH 24/73] automatically retries messages to multisig broker (#5477) adds a server 'ack' message that the server sends to the client upon receiving and successfully parsing the client message. each 'ack' contains the client's message id client sets an interval to retry sending each message. when it receives the 'ack' for its message it clears the interval for that message id --- .../src/multisigBroker/clients/client.ts | 31 ++++++++++++++++++- ironfish-cli/src/multisigBroker/messages.ts | 10 ++++++ ironfish-cli/src/multisigBroker/server.ts | 8 +++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/ironfish-cli/src/multisigBroker/clients/client.ts b/ironfish-cli/src/multisigBroker/clients/client.ts index 07b030e4de..cad3033e57 100644 --- a/ironfish-cli/src/multisigBroker/clients/client.ts +++ b/ironfish-cli/src/multisigBroker/clients/client.ts @@ -22,6 +22,7 @@ import { IdentityMessage, IdentitySchema, JoinSessionMessage, + MultisigBrokerAckSchema, MultisigBrokerMessage, MultisigBrokerMessageSchema, MultisigBrokerMessageWithError, @@ -40,6 +41,7 @@ import { SigningStatusSchema, } from '../messages' +const RETRY_INTERVAL = 5000 export abstract class MultisigClient { readonly logger: Logger readonly version: number @@ -68,6 +70,8 @@ export abstract class MultisigClient { sessionId: string | null = null passphrase: string + retries: Map = new Map() + constructor(options: { passphrase: string; logger: Logger }) { this.logger = options.logger this.version = 3 @@ -147,6 +151,10 @@ export abstract class MultisigClient { if (this.connectTimeout) { clearTimeout(this.connectTimeout) } + + for (const retryInterval of this.retries.values()) { + clearInterval(retryInterval) + } } isConnected(): boolean { @@ -215,14 +223,23 @@ export abstract class MultisigClient { return } + const messageId = this.nextMessageId++ + const message: MultisigBrokerMessage = { - id: this.nextMessageId++, + id: messageId, method, sessionId: this.sessionId, body: this.encryptMessageBody(body), } this.writeData(JSON.stringify(message) + '\n') + + this.retries.set( + messageId, + setInterval(() => { + this.writeData(JSON.stringify(message) + '\n') + }, RETRY_INTERVAL), + ) } protected onConnect(): void { @@ -275,6 +292,18 @@ export abstract class MultisigClient { header.result.body = this.decryptMessageBody(header.result.body) switch (header.result.method) { + case 'ack': { + const body = await YupUtils.tryValidate(MultisigBrokerAckSchema, header.result.body) + + if (body.error) { + throw new ServerMessageMalformedError(body.error, header.result.method) + } + + const retryInterval = this.retries.get(body.result.messageId) + clearInterval(retryInterval) + this.retries.delete(body.result.messageId) + break + } case 'identity': { const body = await YupUtils.tryValidate(IdentitySchema, header.result.body) diff --git a/ironfish-cli/src/multisigBroker/messages.ts b/ironfish-cli/src/multisigBroker/messages.ts index bcdab7997b..8ec4d9c441 100644 --- a/ironfish-cli/src/multisigBroker/messages.ts +++ b/ironfish-cli/src/multisigBroker/messages.ts @@ -18,6 +18,10 @@ export interface MultisigBrokerMessageWithError } } +export type MultisigBrokerAckMessage = { + messageId: number +} + export type DkgStartSessionMessage = { minSigners: number maxSigners: number @@ -94,6 +98,12 @@ export const MultisigBrokerMessageWithErrorSchema: yup.ObjectSchema = yup + .object({ + messageId: yup.number().required(), + }) + .required() + export const DkgStartSessionSchema: yup.ObjectSchema = yup .object({ minSigners: yup.number().defined(), diff --git a/ironfish-cli/src/multisigBroker/server.ts b/ironfish-cli/src/multisigBroker/server.ts index ba2b0dee68..3e7f07b548 100644 --- a/ironfish-cli/src/multisigBroker/server.ts +++ b/ironfish-cli/src/multisigBroker/server.ts @@ -12,6 +12,7 @@ import { DkgStatusMessage, IdentityMessage, IdentitySchema, + MultisigBrokerAckMessage, MultisigBrokerMessage, MultisigBrokerMessageSchema, MultisigBrokerMessageWithError, @@ -179,6 +180,7 @@ export class MultisigServer { } this.logger.debug(`Client ${client.id} sent ${message.method} message`) + this.send(client.socket, 'ack', message.sessionId, { messageId: message.id }) if (message.method === 'dkg.start_session') { await this.handleDkgStartSessionMessage(client, message) @@ -320,6 +322,12 @@ export class MultisigServer { body: SigningStatusMessage, ): void send(socket: net.Socket, method: 'connected', sessionId: string, body: ConnectedMessage): void + send( + socket: net.Socket, + method: 'ack', + sessionId: string, + body: MultisigBrokerAckMessage, + ): void send(socket: net.Socket, method: string, sessionId: string, body?: unknown): void { const message: MultisigBrokerMessage = { id: this.nextMessageId++, From 5e025318092f888b0db9acf1094b22eedd133ebc Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Thu, 3 Oct 2024 14:07:09 -0700 Subject: [PATCH 25/73] multisig broker server broadcasts on step completions (#5476) uses maxSigners for dkg and numSigners for signing to broadcast the session status once the required number of participants have submitted data of a particular type removes broadcasts of individual identities, packages, etc. removes client handlers for messages containing individual identities, packages, etc. simplifies client logic for waiting on data from server: only uses status messages instead of listening for individual identities etc AND polling status distinguishes message methods for dkg identities and signing identities. the message bodies are the same, but it allows the server to distinguish how to handle the session --- .../commands/wallet/multisig/dkg/create.ts | 20 +--- .../src/commands/wallet/multisig/sign.ts | 22 +--- .../src/multisigBroker/clients/client.ts | 71 ++---------- ironfish-cli/src/multisigBroker/server.ts | 109 +++++++++++++----- 4 files changed, 90 insertions(+), 132 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index 8e5f094b19..d74b7413cb 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -405,16 +405,11 @@ export class DkgCreateCommand extends IronfishCommand { errorOnDuplicate: true, }) } else { - multisigClient.submitIdentity(currentIdentity) + multisigClient.submitDkgIdentity(currentIdentity) multisigClient.onDkgStatus.on((message) => { identities = message.identities }) - multisigClient.onIdentity.on((message) => { - if (!identities.includes(message.identity)) { - identities.push(message.identity) - } - }) ux.action.start('Waiting for other Identities from server') while (identities.length < totalParticipants) { @@ -423,7 +418,6 @@ export class DkgCreateCommand extends IronfishCommand { } multisigClient.onDkgStatus.clear() - multisigClient.onIdentity.clear() ux.action.stop() } @@ -511,11 +505,6 @@ export class DkgCreateCommand extends IronfishCommand { multisigClient.onDkgStatus.on((message) => { round1PublicPackages = message.round1PublicPackages }) - multisigClient.onRound1PublicPackage.on((message) => { - if (!round1PublicPackages.includes(message.package)) { - round1PublicPackages.push(message.package) - } - }) ux.action.start('Waiting for other Round 1 Public Packages from server') while (round1PublicPackages.length < totalParticipants) { @@ -524,7 +513,6 @@ export class DkgCreateCommand extends IronfishCommand { } multisigClient.onDkgStatus.clear() - multisigClient.onRound1PublicPackage.clear() ux.action.stop() } @@ -703,11 +691,6 @@ export class DkgCreateCommand extends IronfishCommand { multisigClient.onDkgStatus.on((message) => { round2PublicPackages = message.round2PublicPackages }) - multisigClient.onRound2PublicPackage.on((message) => { - if (!round2PublicPackages.includes(message.package)) { - round2PublicPackages.push(message.package) - } - }) ux.action.start('Waiting for other Round 2 Public Packages from server') while (round2PublicPackages.length < totalParticipants) { @@ -716,7 +699,6 @@ export class DkgCreateCommand extends IronfishCommand { } multisigClient.onDkgStatus.clear() - multisigClient.onRound2PublicPackage.clear() ux.action.stop() } diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts index ce2f38c83a..50391b16d4 100644 --- a/ironfish-cli/src/commands/wallet/multisig/sign.ts +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -239,10 +239,10 @@ export class SignMultisigTransactionCommand extends IronfishCommand { unsignedTransactionHex = message.unsignedTransaction waiting = false }) - multisigClient.getSigningStatus() ux.action.start('Waiting for signer config from server') while (waiting) { + multisigClient.getSigningStatus() await PromiseUtils.sleep(3000) } multisigClient.onSigningStatus.clear() @@ -314,11 +314,6 @@ export class SignMultisigTransactionCommand extends IronfishCommand { multisigClient.onSigningStatus.on((message) => { signatureShares = message.signatureShares }) - multisigClient.onSignatureShare.on((message) => { - if (!signatureShares.includes(message.signatureShare)) { - signatureShares.push(message.signatureShare) - } - }) ux.action.start('Waiting for other Signature Shares from server') while (signatureShares.length < totalParticipants) { @@ -327,7 +322,6 @@ export class SignMultisigTransactionCommand extends IronfishCommand { } multisigClient.onSigningStatus.clear() - multisigClient.onSignatureShare.clear() ux.action.stop() } @@ -448,11 +442,6 @@ export class SignMultisigTransactionCommand extends IronfishCommand { multisigClient.onSigningStatus.on((message) => { commitments = message.signingCommitments }) - multisigClient.onSigningCommitment.on((message) => { - if (!commitments.includes(message.signingCommitment)) { - commitments.push(message.signingCommitment) - } - }) ux.action.start('Waiting for other Signing Commitments from server') while (commitments.length < totalParticipants) { @@ -461,7 +450,6 @@ export class SignMultisigTransactionCommand extends IronfishCommand { } multisigClient.onSigningStatus.clear() - multisigClient.onSigningCommitment.clear() ux.action.stop() } @@ -497,16 +485,11 @@ export class SignMultisigTransactionCommand extends IronfishCommand { errorOnDuplicate: true, }) } else { - multisigClient.submitIdentity(participant.identity) + multisigClient.submitSigningIdentity(participant.identity) multisigClient.onSigningStatus.on((message) => { identities = message.identities }) - multisigClient.onIdentity.on((message) => { - if (!identities.includes(message.identity)) { - identities.push(message.identity) - } - }) ux.action.start('Waiting for other Identities from server') while (identities.length < totalParticipants) { @@ -515,7 +498,6 @@ export class SignMultisigTransactionCommand extends IronfishCommand { } multisigClient.onSigningStatus.clear() - multisigClient.onIdentity.clear() ux.action.stop() } diff --git a/ironfish-cli/src/multisigBroker/clients/client.ts b/ironfish-cli/src/multisigBroker/clients/client.ts index cad3033e57..0f2510f746 100644 --- a/ironfish-cli/src/multisigBroker/clients/client.ts +++ b/ironfish-cli/src/multisigBroker/clients/client.ts @@ -20,7 +20,6 @@ import { DkgStatusMessage, DkgStatusSchema, IdentityMessage, - IdentitySchema, JoinSessionMessage, MultisigBrokerAckSchema, MultisigBrokerMessage, @@ -28,13 +27,9 @@ import { MultisigBrokerMessageWithError, MultisigBrokerMessageWithErrorSchema, Round1PublicPackageMessage, - Round1PublicPackageSchema, Round2PublicPackageMessage, - Round2PublicPackageSchema, SignatureShareMessage, - SignatureShareSchema, SigningCommitmentMessage, - SigningCommitmentSchema, SigningGetStatusMessage, SigningStartSessionMessage, SigningStatusMessage, @@ -57,12 +52,7 @@ export abstract class MultisigClient { private disconnectUntil: number | null = null readonly onConnected = new Event<[]>() - readonly onIdentity = new Event<[IdentityMessage]>() - readonly onRound1PublicPackage = new Event<[Round1PublicPackageMessage]>() - readonly onRound2PublicPackage = new Event<[Round2PublicPackageMessage]>() readonly onDkgStatus = new Event<[DkgStatusMessage]>() - readonly onSigningCommitment = new Event<[SigningCommitmentMessage]>() - readonly onSignatureShare = new Event<[SignatureShareMessage]>() readonly onSigningStatus = new Event<[SigningStatusMessage]>() readonly onConnectedMessage = new Event<[ConnectedMessage]>() readonly onMultisigBrokerError = new Event<[MultisigBrokerMessageWithError]>() @@ -176,8 +166,12 @@ export abstract class MultisigClient { this.send('sign.start_session', { numSigners, unsignedTransaction }) } - submitIdentity(identity: string): void { - this.send('identity', { identity }) + submitDkgIdentity(identity: string): void { + this.send('dkg.identity', { identity }) + } + + submitSigningIdentity(identity: string): void { + this.send('sign.identity', { identity }) } submitRound1PublicPackage(round1PublicPackage: string): void { @@ -207,7 +201,8 @@ export abstract class MultisigClient { private send(method: 'join_session', body: JoinSessionMessage): void private send(method: 'dkg.start_session', body: DkgStartSessionMessage): void private send(method: 'sign.start_session', body: SigningStartSessionMessage): void - private send(method: 'identity', body: IdentityMessage): void + private send(method: 'dkg.identity', body: IdentityMessage): void + private send(method: 'sign.identity', body: IdentityMessage): void private send(method: 'dkg.round1', body: Round1PublicPackageMessage): void private send(method: 'dkg.round2', body: Round2PublicPackageMessage): void private send(method: 'dkg.get_status', body: DkgGetStatusMessage): void @@ -304,36 +299,6 @@ export abstract class MultisigClient { this.retries.delete(body.result.messageId) break } - case 'identity': { - const body = await YupUtils.tryValidate(IdentitySchema, header.result.body) - - if (body.error) { - throw new ServerMessageMalformedError(body.error, header.result.method) - } - - this.onIdentity.emit(body.result) - break - } - case 'dkg.round1': { - const body = await YupUtils.tryValidate(Round1PublicPackageSchema, header.result.body) - - if (body.error) { - throw new ServerMessageMalformedError(body.error, header.result.method) - } - - this.onRound1PublicPackage.emit(body.result) - break - } - case 'dkg.round2': { - const body = await YupUtils.tryValidate(Round2PublicPackageSchema, header.result.body) - - if (body.error) { - throw new ServerMessageMalformedError(body.error, header.result.method) - } - - this.onRound2PublicPackage.emit(body.result) - break - } case 'dkg.status': { const body = await YupUtils.tryValidate(DkgStatusSchema, header.result.body) @@ -344,26 +309,6 @@ export abstract class MultisigClient { this.onDkgStatus.emit(body.result) break } - case 'sign.commitment': { - const body = await YupUtils.tryValidate(SigningCommitmentSchema, header.result.body) - - if (body.error) { - throw new ServerMessageMalformedError(body.error, header.result.method) - } - - this.onSigningCommitment.emit(body.result) - break - } - case 'sign.share': { - const body = await YupUtils.tryValidate(SignatureShareSchema, header.result.body) - - if (body.error) { - throw new ServerMessageMalformedError(body.error, header.result.method) - } - - this.onSignatureShare.emit(body.result) - break - } case 'sign.status': { const body = await YupUtils.tryValidate(SigningStatusSchema, header.result.body) diff --git a/ironfish-cli/src/multisigBroker/server.ts b/ironfish-cli/src/multisigBroker/server.ts index 3e7f07b548..930f00fedd 100644 --- a/ironfish-cli/src/multisigBroker/server.ts +++ b/ironfish-cli/src/multisigBroker/server.ts @@ -10,19 +10,14 @@ import { DkgGetStatusSchema, DkgStartSessionSchema, DkgStatusMessage, - IdentityMessage, IdentitySchema, MultisigBrokerAckMessage, MultisigBrokerMessage, MultisigBrokerMessageSchema, MultisigBrokerMessageWithError, - Round1PublicPackageMessage, Round1PublicPackageSchema, - Round2PublicPackageMessage, Round2PublicPackageSchema, - SignatureShareMessage, SignatureShareSchema, - SigningCommitmentMessage, SigningCommitmentSchema, SigningGetStatusSchema, SigningStartSessionSchema, @@ -191,8 +186,8 @@ export class MultisigServer { } else if (message.method === 'join_session') { this.handleJoinSessionMessage(client, message) return - } else if (message.method === 'identity') { - await this.handleIdentityMessage(client, message) + } else if (message.method === 'dkg.identity') { + await this.handleDkgIdentityMessage(client, message) return } else if (message.method === 'dkg.round1') { await this.handleRound1PublicPackageMessage(client, message) @@ -203,6 +198,9 @@ export class MultisigServer { } else if (message.method === 'dkg.get_status') { await this.handleDkgGetStatusMessage(client, message) return + } else if (message.method === 'sign.identity') { + await this.handleSigningIdentityMessage(client, message) + return } else if (message.method === 'sign.commitment') { await this.handleSigningCommitmentMessage(client, message) return @@ -250,23 +248,8 @@ export class MultisigServer { this.logger.debug(`Session ${sessionId} cleaned up. Active sessions: ${this.sessions.size}`) } - private broadcast(method: 'identity', sessionId: string, body: IdentityMessage): void - private broadcast( - method: 'dkg.round1', - sessionId: string, - body: Round1PublicPackageMessage, - ): void - private broadcast( - method: 'dkg.round2', - sessionId: string, - body: Round2PublicPackageMessage, - ): void - private broadcast( - method: 'sign.commitment', - sessionId: string, - body: SigningCommitmentMessage, - ): void - private broadcast(method: 'sign.share', sessionId: string, body: SignatureShareMessage): void + private broadcast(method: 'dkg.status', sessionId: string, body?: DkgStatusMessage): void + private broadcast(method: 'sign.status', sessionId: string, body?: SigningStatusMessage): void private broadcast(method: string, sessionId: string, body?: unknown): void { const message: MultisigBrokerMessage = { id: this.nextMessageId++, @@ -435,7 +418,7 @@ export class MultisigServer { client.sessionId = message.sessionId } - async handleIdentityMessage(client: MultisigServerClient, message: MultisigBrokerMessage) { + async handleDkgIdentityMessage(client: MultisigServerClient, message: MultisigBrokerMessage) { const body = await YupUtils.tryValidate(IdentitySchema, message.body) if (body.error) { @@ -448,11 +431,61 @@ export class MultisigServer { return } + if (!isDkgSession(session)) { + this.sendErrorMessage( + client, + message.id, + `Session is not a dkg session: ${message.sessionId}`, + ) + return + } + const identity = body.result.identity if (!session.status.identities.includes(identity)) { session.status.identities.push(identity) this.sessions.set(message.sessionId, session) - this.broadcast('identity', message.sessionId, { identity }) + + // Broadcast status after collecting all identities + if (session.status.identities.length === session.status.maxSigners) { + this.broadcast('dkg.status', message.sessionId, session.status) + } + } + } + + async handleSigningIdentityMessage( + client: MultisigServerClient, + message: MultisigBrokerMessage, + ) { + const body = await YupUtils.tryValidate(IdentitySchema, message.body) + + if (body.error) { + return + } + + const session = this.sessions.get(message.sessionId) + if (!session) { + this.sendErrorMessage(client, message.id, `Session not found: ${message.sessionId}`) + return + } + + if (!isSigningSession(session)) { + this.sendErrorMessage( + client, + message.id, + `Session is not a signing session: ${message.sessionId}`, + ) + return + } + + const identity = body.result.identity + if (!session.status.identities.includes(identity)) { + session.status.identities.push(identity) + this.sessions.set(message.sessionId, session) + + // Broadcast status after collecting all identities + if (session.status.identities.length === session.status.numSigners) { + this.broadcast('sign.status', message.sessionId, session.status) + } } } @@ -485,7 +518,11 @@ export class MultisigServer { if (!session.status.round1PublicPackages.includes(round1PublicPackage)) { session.status.round1PublicPackages.push(round1PublicPackage) this.sessions.set(message.sessionId, session) - this.broadcast('dkg.round1', message.sessionId, { package: round1PublicPackage }) + + // Broadcast status after collecting all packages + if (session.status.round1PublicPackages.length === session.status.maxSigners) { + this.broadcast('dkg.status', message.sessionId, session.status) + } } } @@ -518,7 +555,11 @@ export class MultisigServer { if (!session.status.round2PublicPackages.includes(round2PublicPackage)) { session.status.round2PublicPackages.push(round2PublicPackage) this.sessions.set(message.sessionId, session) - this.broadcast('dkg.round2', message.sessionId, { package: round2PublicPackage }) + + // Broadcast status after collecting all packages + if (session.status.round2PublicPackages.length === session.status.maxSigners) { + this.broadcast('dkg.status', message.sessionId, session.status) + } } } @@ -579,7 +620,11 @@ export class MultisigServer { if (!session.status.signingCommitments.includes(signingCommitment)) { session.status.signingCommitments.push(signingCommitment) this.sessions.set(message.sessionId, session) - this.broadcast('sign.commitment', message.sessionId, { signingCommitment }) + + // Broadcast status after collecting all signing commitments + if (session.status.signingCommitments.length === session.status.numSigners) { + this.broadcast('sign.status', message.sessionId, session.status) + } } } @@ -612,7 +657,11 @@ export class MultisigServer { if (!session.status.signatureShares.includes(signatureShare)) { session.status.signatureShares.push(signatureShare) this.sessions.set(message.sessionId, session) - this.broadcast('sign.share', message.sessionId, { signatureShare }) + + // Broadcast status after collecting all signature shares + if (session.status.signatureShares.length === session.status.numSigners) { + this.broadcast('sign.status', message.sessionId, session.status) + } } } From 015d818dc8bb3215e0f28ce92ce549676f90c1b2 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Thu, 3 Oct 2024 15:20:56 -0700 Subject: [PATCH 26/73] Cleanup DKG identity creation (#5473) * Cleanup DKG identity creation * Remove unused functions --- .../commands/wallet/multisig/dkg/create.ts | 108 +++++++----------- 1 file changed, 41 insertions(+), 67 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index d74b7413cb..041da333f0 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -130,16 +130,11 @@ export class DkgCreateCommand extends IronfishCommand { } } - const { name: participantName, identity } = ledger - ? await ui.retryStep( - () => { - Assert.isNotUndefined(ledger) - return this.getIdentityFromLedger(ledger, client, accountName) - }, - this.logger, - true, - ) - : await this.getParticipant(client, accountName) + const { name: participantName, identity } = await this.getOrCreateIdentity( + client, + ledger, + accountName, + ) const { totalParticipants, minSigners } = await ui.retryStep( async () => { @@ -202,11 +197,46 @@ export class DkgCreateCommand extends IronfishCommand { multisigClient?.stop() } - private async getParticipant(client: RpcClient, name: string) { + private async getOrCreateIdentity( + client: RpcClient, + ledger: LedgerMultiSigner | undefined, + name: string, + ): Promise<{ + identity: string + name: string + }> { const identities = await client.wallet.multisig.getIdentities() + + if (ledger) { + const ledgerIdentity = await ledger.dkgGetIdentity(0) + + const foundIdentity = identities.content.identities.find( + (i) => i.identity === ledgerIdentity.toString('hex'), + ) + + if (foundIdentity) { + this.debug('Identity from ledger already exists') + return foundIdentity + } + + // We must use the ledger's identity + while (identities.content.identities.find((i) => i.name === name)) { + this.log('An identity with the same name already exists') + name = await ui.inputPrompt('Enter a new name for the identity', true) + } + + const created = await client.wallet.multisig.importParticipant({ + name, + identity: ledgerIdentity.toString('hex'), + }) + + return { name, identity: created.content.identity } + } + const foundIdentity = identities.content.identities.find((i) => i.name === name) if (foundIdentity) { + this.debug(`Identity already exists with name: ${foundIdentity.name}`) return foundIdentity } @@ -235,62 +265,6 @@ export class DkgCreateCommand extends IronfishCommand { return name } - async getIdentityFromLedger( - ledger: LedgerMultiSigner, - client: RpcClient, - name: string, - ): Promise<{ - name: string - identity: string - }> { - // TODO(hughy): support multiple identities using index - const identity = await ledger.dkgGetIdentity(0) - - const allIdentities = (await client.wallet.multisig.getIdentities()).content.identities - - const foundIdentity = allIdentities.find((i) => i.identity === identity.toString('hex')) - - if (foundIdentity) { - this.log(`Identity already exists with name: ${foundIdentity.name}`) - - return { - name: foundIdentity.name, - identity: identity.toString('hex'), - } - } - - name = await ui.inputPrompt('Enter a name for the identity', true) - - while (allIdentities.find((i) => i.name === name)) { - this.log('An identity with the same name already exists') - name = await ui.inputPrompt('Enter a new name for the identity', true) - } - - await client.wallet.multisig.importParticipant({ - name, - identity: identity.toString('hex'), - }) - - return { - name, - identity: identity.toString('hex'), - } - } - - async createParticipant( - client: RpcClient, - name: string, - ): Promise<{ - name: string - identity: string - }> { - const identity = (await client.wallet.multisig.createParticipant({ name })).content.identity - return { - name, - identity, - } - } - async getDkgConfig( multisigClient: MultisigClient | null, ledger: boolean, From 5aa61592dcad321082b9f7e7970cff2af83a8b24 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Thu, 3 Oct 2024 16:31:41 -0700 Subject: [PATCH 27/73] prevents incorrect session encryption from halting process (#5483) if a client submits data to the broker server encrypted with the wrong passphrase and key then all other clients will now skip that data adds a 'challenge' to the session which is a string encrypted with the session passphrase and key if a client fails to decrypt the challenge then the client throws an error decrypts message data only when necessary --- .../src/multisigBroker/clients/client.ts | 61 ++++++++++++++----- ironfish-cli/src/multisigBroker/messages.ts | 14 +++++ ironfish-cli/src/multisigBroker/server.ts | 17 +++++- 3 files changed, 76 insertions(+), 16 deletions(-) diff --git a/ironfish-cli/src/multisigBroker/clients/client.ts b/ironfish-cli/src/multisigBroker/clients/client.ts index 0f2510f746..cff5cf4fa8 100644 --- a/ironfish-cli/src/multisigBroker/clients/client.ts +++ b/ironfish-cli/src/multisigBroker/clients/client.ts @@ -20,6 +20,8 @@ import { DkgStatusMessage, DkgStatusSchema, IdentityMessage, + JoinedSessionMessage, + JoinedSessionSchema, JoinSessionMessage, MultisigBrokerAckSchema, MultisigBrokerMessage, @@ -55,6 +57,7 @@ export abstract class MultisigClient { readonly onDkgStatus = new Event<[DkgStatusMessage]>() readonly onSigningStatus = new Event<[SigningStatusMessage]>() readonly onConnectedMessage = new Event<[ConnectedMessage]>() + readonly onJoinedSession = new Event<[JoinSessionMessage]>() readonly onMultisigBrokerError = new Event<[MultisigBrokerMessageWithError]>() sessionId: string | null = null @@ -158,12 +161,14 @@ export abstract class MultisigClient { startDkgSession(maxSigners: number, minSigners: number): void { this.sessionId = uuid() - this.send('dkg.start_session', { maxSigners, minSigners }) + const challenge = this.key.encrypt(Buffer.from('DKG')).toString('hex') + this.send('dkg.start_session', { maxSigners, minSigners, challenge }) } startSigningSession(numSigners: number, unsignedTransaction: string): void { this.sessionId = uuid() - this.send('sign.start_session', { numSigners, unsignedTransaction }) + const challenge = this.key.encrypt(Buffer.from('SIGNING')).toString('hex') + this.send('sign.start_session', { numSigners, unsignedTransaction, challenge }) } submitDkgIdentity(identity: string): void { @@ -254,6 +259,9 @@ export abstract class MultisigClient { } protected onError = (error: unknown): void => { + if (error instanceof SessionDecryptionError) { + throw error + } this.logger.error(`Error ${ErrorUtils.renderError(error)}`) } @@ -283,9 +291,6 @@ export abstract class MultisigClient { this.logger.debug(`Server sent ${header.result.method} message`) - // Decrypt fields in the message body - header.result.body = this.decryptMessageBody(header.result.body) - switch (header.result.method) { case 'ack': { const body = await YupUtils.tryValidate(MultisigBrokerAckSchema, header.result.body) @@ -306,7 +311,8 @@ export abstract class MultisigClient { throw new ServerMessageMalformedError(body.error, header.result.method) } - this.onDkgStatus.emit(body.result) + const decrypted = this.decryptMessageBody(body.result) + this.onDkgStatus.emit(decrypted) break } case 'sign.status': { @@ -316,7 +322,8 @@ export abstract class MultisigClient { throw new ServerMessageMalformedError(body.error, header.result.method) } - this.onSigningStatus.emit(body.result) + const decrypted = this.decryptMessageBody(body.result) + this.onSigningStatus.emit(decrypted) break } case 'connected': { @@ -329,6 +336,22 @@ export abstract class MultisigClient { this.onConnectedMessage.emit(body.result) break } + case 'joined_session': { + const body = await YupUtils.tryValidate(JoinedSessionSchema, header.result.body) + if (body.error) { + throw new ServerMessageMalformedError(body.error, header.result.method) + } + + try { + const decrypted = this.decryptMessageBody(body.result) + this.onJoinedSession.emit(decrypted) + break + } catch { + throw new SessionDecryptionError( + 'Failed to decrypt session challenge. Passphrase is incorrect.', + ) + } + } default: throw new ServerMessageMalformedError(`Invalid message ${header.result.method}`) @@ -363,13 +386,9 @@ export abstract class MultisigClient { return encrypted } - private decryptMessageBody(body?: unknown): object | undefined { - if (!body) { - return - } - - let decrypted = body as object - for (const [key, value] of Object.entries(body as object)) { + private decryptMessageBody(body: T): T { + let decrypted = body + for (const [key, value] of Object.entries(body)) { if (typeof value === 'string') { decrypted = { ...decrypted, @@ -379,7 +398,13 @@ export abstract class MultisigClient { const decryptedItems = [] for (const item of value) { if (typeof item === 'string') { - decryptedItems.push(this.key.decrypt(Buffer.from(item, 'hex')).toString()) + try { + decryptedItems.push(this.key.decrypt(Buffer.from(item, 'hex')).toString()) + } catch { + this.logger.debug( + 'Failed to decrypt submitted session data. Skipping invalid data.', + ) + } } else { decryptedItems.push(item) } @@ -394,3 +419,9 @@ export abstract class MultisigClient { return decrypted } } + +class SessionDecryptionError extends Error { + constructor(message: string) { + super(message) + } +} diff --git a/ironfish-cli/src/multisigBroker/messages.ts b/ironfish-cli/src/multisigBroker/messages.ts index 8ec4d9c441..8775a219e6 100644 --- a/ironfish-cli/src/multisigBroker/messages.ts +++ b/ironfish-cli/src/multisigBroker/messages.ts @@ -25,15 +25,21 @@ export type MultisigBrokerAckMessage = { export type DkgStartSessionMessage = { minSigners: number maxSigners: number + challenge: string } export type SigningStartSessionMessage = { numSigners: number unsignedTransaction: string + challenge: string } export type JoinSessionMessage = object | undefined +export type JoinedSessionMessage = { + challenge: string +} + export type IdentityMessage = { identity: string } @@ -108,6 +114,7 @@ export const DkgStartSessionSchema: yup.ObjectSchema = y .object({ minSigners: yup.number().defined(), maxSigners: yup.number().defined(), + challenge: yup.string().defined(), }) .defined() @@ -115,6 +122,7 @@ export const SigningStartSessionSchema: yup.ObjectSchema = yup .notRequired() .default(undefined) +export const JoinedSessionSchema: yup.ObjectSchema = yup + .object({ + challenge: yup.string().required(), + }) + .required() + export const IdentitySchema: yup.ObjectSchema = yup .object({ identity: yup.string().defined(), diff --git a/ironfish-cli/src/multisigBroker/server.ts b/ironfish-cli/src/multisigBroker/server.ts index 930f00fedd..0ddc95bae3 100644 --- a/ironfish-cli/src/multisigBroker/server.ts +++ b/ironfish-cli/src/multisigBroker/server.ts @@ -11,6 +11,7 @@ import { DkgStartSessionSchema, DkgStatusMessage, IdentitySchema, + JoinedSessionMessage, MultisigBrokerAckMessage, MultisigBrokerMessage, MultisigBrokerMessageSchema, @@ -34,6 +35,7 @@ interface MultisigSession { id: string type: MultisigSessionType status: DkgStatus | SigningStatus + challenge: string } interface DkgSession extends MultisigSession { @@ -305,6 +307,12 @@ export class MultisigServer { body: SigningStatusMessage, ): void send(socket: net.Socket, method: 'connected', sessionId: string, body: ConnectedMessage): void + send( + socket: net.Socket, + method: 'joined_session', + sessionId: string, + body: JoinedSessionMessage, + ): void send( socket: net.Socket, method: 'ack', @@ -362,6 +370,7 @@ export class MultisigServer { round1PublicPackages: [], round2PublicPackages: [], }, + challenge: body.result.challenge, } this.sessions.set(sessionId, session) @@ -398,6 +407,7 @@ export class MultisigServer { signingCommitments: [], signatureShares: [], }, + challenge: body.result.challenge, } this.sessions.set(sessionId, session) @@ -408,7 +418,8 @@ export class MultisigServer { } handleJoinSessionMessage(client: MultisigServerClient, message: MultisigBrokerMessage) { - if (!this.sessions.has(message.sessionId)) { + const session = this.sessions.get(message.sessionId) + if (!session) { this.sendErrorMessage(client, message.id, `Session not found: ${message.sessionId}`) return } @@ -416,6 +427,10 @@ export class MultisigServer { this.logger.debug(`Client ${client.id} joined session ${message.sessionId}`) client.sessionId = message.sessionId + + this.send(client.socket, 'joined_session', message.sessionId, { + challenge: session.challenge, + }) } async handleDkgIdentityMessage(client: MultisigServerClient, message: MultisigBrokerMessage) { From f3f82af470b6d3c8f40556283c0e35672e9164a6 Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Thu, 3 Oct 2024 16:56:59 -0700 Subject: [PATCH 28/73] Add ability for sessions to track specific clients (#5482) * Add ability for sessions to track specific clients * Remove extra client ids so the data becomes correct --- ironfish-cli/src/multisigBroker/server.ts | 70 +++++++++++++++++++---- 1 file changed, 59 insertions(+), 11 deletions(-) diff --git a/ironfish-cli/src/multisigBroker/server.ts b/ironfish-cli/src/multisigBroker/server.ts index 0ddc95bae3..2c1d9564f7 100644 --- a/ironfish-cli/src/multisigBroker/server.ts +++ b/ironfish-cli/src/multisigBroker/server.ts @@ -34,6 +34,7 @@ enum MultisigSessionType { interface MultisigSession { id: string type: MultisigSessionType + clientIds: Set status: DkgStatus | SigningStatus challenge: string } @@ -156,8 +157,14 @@ export class MultisigServer { client.socket.removeAllListeners('close') client.socket.removeAllListeners('error') - if (client.sessionId && !this.isSessionActive(client.sessionId)) { - this.cleanupSession(client.sessionId) + if (client.sessionId) { + const sessionId = client.sessionId + + this.removeClientFromSession(client) + + if (!this.isSessionActive(sessionId)) { + this.cleanupSession(sessionId) + } } } @@ -237,11 +244,15 @@ export class MultisigServer { * session should still be considered active */ private isSessionActive(sessionId: string): boolean { - for (const client of this.clients.values()) { - if (client.connected && client.sessionId && client.sessionId === sessionId) { - return true - } + const session = this.sessions.get(sessionId) + if (!session) { + return false + } + + if (session.clientIds.size > 0) { + return true } + return false } @@ -250,6 +261,30 @@ export class MultisigServer { this.logger.debug(`Session ${sessionId} cleaned up. Active sessions: ${this.sessions.size}`) } + private addClientToSession(client: MultisigServerClient, sessionId: string): void { + const session = this.sessions.get(sessionId) + if (!session) { + return + } + + client.sessionId = session.id + session.clientIds.add(client.id) + } + + private removeClientFromSession(client: MultisigServerClient): void { + if (!client.sessionId) { + return + } + + const session = this.sessions.get(client.sessionId) + if (!session) { + return + } + + client.sessionId = null + session.clientIds.delete(client.id) + } + private broadcast(method: 'dkg.status', sessionId: string, body?: DkgStatusMessage): void private broadcast(method: 'sign.status', sessionId: string, body?: SigningStatusMessage): void private broadcast(method: string, sessionId: string, body?: unknown): void { @@ -272,8 +307,19 @@ export class MultisigServer { let broadcasted = 0 - for (const client of this.clients.values()) { - if (client.sessionId !== sessionId) { + const session = this.sessions.get(sessionId) + if (!session) { + this.logger.debug(`Session ${sessionId} does not exist, broadcast failed`) + return + } + + for (const clientId of session.clientIds) { + const client = this.clients.get(clientId) + if (!client) { + this.logger.debug( + `Client ${clientId} does not exist, but session ${sessionId} thinks it does, removing.`, + ) + session.clientIds.delete(clientId) continue } @@ -363,6 +409,7 @@ export class MultisigServer { const session = { id: sessionId, type: MultisigSessionType.DKG, + clientIds: new Set(), status: { maxSigners: body.result.maxSigners, minSigners: body.result.minSigners, @@ -377,7 +424,7 @@ export class MultisigServer { this.logger.debug(`Client ${client.id} started dkg session ${message.sessionId}`) - client.sessionId = message.sessionId + this.addClientToSession(client, sessionId) } async handleSigningStartSessionMessage( @@ -400,6 +447,7 @@ export class MultisigServer { const session = { id: sessionId, type: MultisigSessionType.SIGNING, + clientIds: new Set(), status: { numSigners: body.result.numSigners, unsignedTransaction: body.result.unsignedTransaction, @@ -414,7 +462,7 @@ export class MultisigServer { this.logger.debug(`Client ${client.id} started signing session ${message.sessionId}`) - client.sessionId = message.sessionId + this.addClientToSession(client, sessionId) } handleJoinSessionMessage(client: MultisigServerClient, message: MultisigBrokerMessage) { @@ -426,7 +474,7 @@ export class MultisigServer { this.logger.debug(`Client ${client.id} joined session ${message.sessionId}`) - client.sessionId = message.sessionId + this.addClientToSession(client, message.sessionId) this.send(client.socket, 'joined_session', message.sessionId, { challenge: session.challenge, From d3edfa1650780a145a4a8cdeb0dce4e60d326439 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Thu, 3 Oct 2024 17:11:32 -0700 Subject: [PATCH 29/73] updates ux status messages waiting for multisig broker (#5484) * updates ux status messages waiting for multisig broker displays the number of identities, packages, commitments, etc. that the client has received from the server and how many are expected Closes IFL-3016 * prevents status from showing '0/n' initializes lists with user's own packages so that status reflects that user has submitted data --- .../src/commands/wallet/multisig/dkg/create.ts | 15 +++++++++------ ironfish-cli/src/commands/wallet/multisig/sign.ts | 15 +++++++++------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index 041da333f0..cd013b7997 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -365,7 +365,7 @@ export class DkgCreateCommand extends IronfishCommand { }> { this.log('\nCollecting Participant Info and Performing Round 1...') - let identities: string[] = [] + let identities: string[] = [currentIdentity] if (!multisigClient) { this.log(`Identity for ${participantName}: \n${currentIdentity} \n`) @@ -385,9 +385,10 @@ export class DkgCreateCommand extends IronfishCommand { identities = message.identities }) - ux.action.start('Waiting for other Identities from server') + ux.action.start('Waiting for Identities from server') while (identities.length < totalParticipants) { multisigClient.getDkgStatus() + ux.action.status = `${identities.length}/${totalParticipants}` await PromiseUtils.sleep(3000) } @@ -453,7 +454,7 @@ export class DkgCreateCommand extends IronfishCommand { round2: { secretPackage: string; publicPackage: string } round1PublicPackages: string[] }> { - let round1PublicPackages: string[] = [] + let round1PublicPackages: string[] = [round1Result.publicPackage] if (!multisigClient) { this.log('\n============================================') this.debug('\nRound 1 Encrypted Secret Package:') @@ -480,9 +481,10 @@ export class DkgCreateCommand extends IronfishCommand { round1PublicPackages = message.round1PublicPackages }) - ux.action.start('Waiting for other Round 1 Public Packages from server') + ux.action.start('Waiting for Round 1 Public Packages from server') while (round1PublicPackages.length < totalParticipants) { multisigClient.getDkgStatus() + ux.action.status = `${round1PublicPackages.length}/${totalParticipants}` await PromiseUtils.sleep(3000) } @@ -639,7 +641,7 @@ export class DkgCreateCommand extends IronfishCommand { ledger: LedgerMultiSigner | undefined, accountCreatedAt?: number, ): Promise { - let round2PublicPackages: string[] = [] + let round2PublicPackages: string[] = [round2Result.publicPackage] if (!multisigClient) { this.log('\n============================================') this.debug('\nRound 2 Encrypted Secret Package:') @@ -666,9 +668,10 @@ export class DkgCreateCommand extends IronfishCommand { round2PublicPackages = message.round2PublicPackages }) - ux.action.start('Waiting for other Round 2 Public Packages from server') + ux.action.start('Waiting for Round 2 Public Packages from server') while (round2PublicPackages.length < totalParticipants) { multisigClient.getDkgStatus() + ux.action.status = `${round2PublicPackages.length}/${totalParticipants}` await PromiseUtils.sleep(3000) } diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts index 50391b16d4..938f273964 100644 --- a/ironfish-cli/src/commands/wallet/multisig/sign.ts +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -289,7 +289,7 @@ export class SignMultisigTransactionCommand extends IronfishCommand { signatureShare: string, totalParticipants: number, ): Promise { - let signatureShares: string[] = [] + let signatureShares: string[] = [signatureShare] if (!multisigClient) { this.log('\n============================================') this.log('\nSignature Share:') @@ -315,9 +315,10 @@ export class SignMultisigTransactionCommand extends IronfishCommand { signatureShares = message.signatureShares }) - ux.action.start('Waiting for other Signature Shares from server') + ux.action.start('Waiting for Signature Shares from server') while (signatureShares.length < totalParticipants) { multisigClient.getSigningStatus() + ux.action.status = `${signatureShares.length}/${totalParticipants}` await PromiseUtils.sleep(3000) } @@ -419,7 +420,7 @@ export class SignMultisigTransactionCommand extends IronfishCommand { totalParticipants: number, unsignedTransaction: UnsignedTransaction, ) { - let commitments: string[] = [] + let commitments: string[] = [commitment] if (!multisigClient) { this.log('\n============================================') this.log('\nCommitment:') @@ -443,9 +444,10 @@ export class SignMultisigTransactionCommand extends IronfishCommand { commitments = message.signingCommitments }) - ux.action.start('Waiting for other Signing Commitments from server') + ux.action.start('Waiting for Signing Commitments from server') while (commitments.length < totalParticipants) { multisigClient.getSigningStatus() + ux.action.status = `${commitments.length}/${totalParticipants}` await PromiseUtils.sleep(3000) } @@ -471,7 +473,7 @@ export class SignMultisigTransactionCommand extends IronfishCommand { unsignedTransaction: UnsignedTransaction, ledger: LedgerMultiSigner | undefined, ) { - let identities: string[] = [] + let identities: string[] = [participant.identity] if (!multisigClient) { this.log(`Identity for ${participant.name}: \n${participant.identity} \n`) this.log('Share your participant identity with other signers.') @@ -491,9 +493,10 @@ export class SignMultisigTransactionCommand extends IronfishCommand { identities = message.identities }) - ux.action.start('Waiting for other Identities from server') + ux.action.start('Waiting for Identities from server') while (identities.length < totalParticipants) { multisigClient.getSigningStatus() + ux.action.status = `${identities.length}/${totalParticipants}` await PromiseUtils.sleep(3000) } From 5f1d60d024329fb29071591ccb69793c338b748d Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Sun, 6 Oct 2024 21:40:16 -0700 Subject: [PATCH 30/73] Use ledger action to make ledger more reliable (#5474) * Add UI action to make ledger reliable This UI action will handle many of the failure cases that occur when running ledger commands. They'll use CLI-UX to inform the user of the current state, and what the state should be. * UX improvements --- .../commands/wallet/multisig/dkg/create.ts | 80 ++++++---- ironfish-cli/src/ledger/ledger.ts | 145 ++++++++++++------ ironfish-cli/src/ledger/ledgerMultiSigner.ts | 7 +- ironfish-cli/src/ui/index.ts | 1 + ironfish-cli/src/ui/ledger.ts | 103 +++++++++++++ 5 files changed, 256 insertions(+), 80 deletions(-) create mode 100644 ironfish-cli/src/ui/ledger.ts diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index cd013b7997..91455a0cd1 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -71,15 +71,6 @@ export class DkgCreateCommand extends IronfishCommand { if (flags.ledger) { ledger = new LedgerMultiSigner(this.logger) - try { - await ledger.connect() - } catch (e) { - if (e instanceof Error) { - this.error(e.message) - } else { - throw e - } - } } const accountName = await this.getAccountName(client, flags.name ?? flags.participant) @@ -208,7 +199,11 @@ export class DkgCreateCommand extends IronfishCommand { const identities = await client.wallet.multisig.getIdentities() if (ledger) { - const ledgerIdentity = await ledger.dkgGetIdentity(0) + const ledgerIdentity = await ui.ledger({ + ledger, + message: 'Getting Ledger Identity', + action: () => ledger.dkgGetIdentity(0), + }) const foundIdentity = identities.content.identities.find( (i) => i.identity === ledgerIdentity.toString('hex'), @@ -342,7 +337,12 @@ export class DkgCreateCommand extends IronfishCommand { } // TODO(hughy): determine how to handle multiple identities using index - const { publicPackage, secretPackage } = await ledger.dkgRound1(0, identities, minSigners) + const { publicPackage, secretPackage } = await ui.ledger({ + ledger, + message: 'Round1 on Ledger', + approval: true, + action: () => ledger.dkgRound1(0, identities, minSigners), + }) return { round1: { @@ -429,11 +429,12 @@ export class DkgCreateCommand extends IronfishCommand { round2: { secretPackage: string; publicPackage: string } }> { // TODO(hughy): determine how to handle multiple identities using index - const { publicPackage, secretPackage } = await ledger.dkgRound2( - 0, - round1PublicPackages, - round1SecretPackage, - ) + const { publicPackage, secretPackage } = await ui.ledger({ + ledger, + message: 'Round2 on Ledger', + approval: true, + action: () => ledger.dkgRound2(0, round1PublicPackages, round1SecretPackage), + }) return { round2: { @@ -550,9 +551,9 @@ export class DkgCreateCommand extends IronfishCommand { .sort((a, b) => a.senderIdentity.localeCompare(b.senderIdentity)) // Extract raw parts from round1 and round2 public packages - const participants = [] - const round1FrostPackages = [] - const gskBytes = [] + const participants: string[] = [] + const round1FrostPackages: string[] = [] + const gskBytes: string[] = [] for (const pkg of round1PublicPackages) { // Exclude participant's own identity and round1 public package if (pkg.identity !== identity) { @@ -566,19 +567,33 @@ export class DkgCreateCommand extends IronfishCommand { const round2FrostPackages = round2PublicPackages.map((pkg) => pkg.frostPackage) // Perform round3 with Ledger - await ledger.dkgRound3( - 0, - participants, - round1FrostPackages, - round2FrostPackages, - round2SecretPackage, - gskBytes, - ) + await ui.ledger({ + ledger, + message: 'Round3 on Ledger', + approval: true, + action: () => + ledger.dkgRound3( + 0, + participants, + round1FrostPackages, + round2FrostPackages, + round2SecretPackage, + gskBytes, + ), + }) // Retrieve all multisig account keys and publicKeyPackage - const dkgKeys = await ledger.dkgRetrieveKeys() + const dkgKeys = await ui.ledger({ + ledger, + message: 'Getting Ledger DKG keys', + action: () => ledger.dkgRetrieveKeys(), + }) - const publicKeyPackage = await ledger.dkgGetPublicPackage() + const publicKeyPackage = await ui.ledger({ + ledger, + message: 'Getting Ledger Public Package', + action: () => ledger.dkgGetPublicPackage(), + }) const accountImport = { ...dkgKeys, @@ -606,7 +621,12 @@ export class DkgCreateCommand extends IronfishCommand { this.log('Creating an encrypted backup of multisig keys from your Ledger device...') this.log() - const encryptedKeys = await ledger.dkgBackupKeys() + const encryptedKeys = await ui.ledger({ + ledger, + message: 'Backup DKG Keys', + approval: true, + action: () => ledger.dkgBackupKeys(), + }) this.log() this.log('Encrypted Ledger Multisig Backup:') diff --git a/ironfish-cli/src/ledger/ledger.ts b/ironfish-cli/src/ledger/ledger.ts index b91da1c07b..53b71aeddb 100644 --- a/ironfish-cli/src/ledger/ledger.ts +++ b/ironfish-cli/src/ledger/ledger.ts @@ -2,6 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { Assert, createRootLogger, Logger } from '@ironfish/sdk' +import { StatusCodes as LedgerStatusCodes, TransportStatusError } from '@ledgerhq/errors' import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' import IronfishApp, { KeyResponse, @@ -9,13 +10,14 @@ import IronfishApp, { ResponseProofGenKey, ResponseViewKey, } from '@zondax/ledger-ironfish' -import { ResponseError } from '@zondax/ledger-js' +import { ResponseError, Transport } from '@zondax/ledger-js' export class Ledger { app: IronfishApp | undefined logger: Logger PATH = "m/44'/1338'/0" isMultisig: boolean + isConnecting: boolean = false constructor(isMultisig: boolean, logger?: Logger) { this.app = undefined @@ -24,21 +26,64 @@ export class Ledger { } tryInstruction = async (instruction: (app: IronfishApp) => Promise) => { - await this.refreshConnection() - Assert.isNotUndefined(this.app, 'Unable to establish connection with Ledger device') - try { + await this.connect() + + Assert.isNotUndefined(this.app, 'Unable to establish connection with Ledger device') + + // App info is a request to the dashboard CLA. The purpose of this it to + // produce a Locked Device error and works if an app is open or closed. + await this.app.appInfo() + + // This is an app specific request. This is useful because this throws + // INS_NOT_SUPPORTED in the case that the app is locked which is useful to + // know versus the device is locked. + try { + await this.app.getVersion() + } catch (error) { + if ( + error instanceof ResponseError && + error.returnCode === LedgerStatusCodes.INS_NOT_SUPPORTED + ) { + throw new LedgerAppLocked() + } + + throw error + } + return await instruction(this.app) } catch (error: unknown) { - if (isResponseError(error)) { - this.logger.debug(`Ledger ResponseError returnCode: ${error.returnCode.toString(16)}`) - if (error.returnCode === LedgerDeviceLockedError.returnCode) { - throw new LedgerDeviceLockedError('Please unlock your Ledger device.') - } else if (LedgerAppUnavailableError.returnCodes.includes(error.returnCode)) { - throw new LedgerAppUnavailableError() + if (LedgerPortIsBusyError.IsError(error)) { + throw new LedgerPortIsBusyError() + } else if (LedgerConnectError.IsError(error)) { + throw new LedgerConnectError() + } + + if (error instanceof TransportStatusError) { + throw new LedgerConnectError() + } + + if (error instanceof ResponseError) { + if (error.returnCode === LedgerStatusCodes.LOCKED_DEVICE) { + throw new LedgerDeviceLockedError() + } else if (error.returnCode === LedgerStatusCodes.CLA_NOT_SUPPORTED) { + throw new LedgerClaNotSupportedError() + } else if (error.returnCode === LedgerStatusCodes.GP_AUTH_FAILED) { + throw new LedgerGPAuthFailed() + } else if ( + [ + LedgerStatusCodes.INS_NOT_SUPPORTED, + LedgerStatusCodes.TECHNICAL_PROBLEM, + 0xffff, // Unknown transport error + 0x6e01, // App not open + ].includes(error.returnCode) + ) { + throw new LedgerAppNotOpen( + `Unable to connect to Ironfish app on Ledger. Please check that the device is unlocked and the app is open.`, + ) } - throw new LedgerError(error.errorMessage) + throw new LedgerError(error.message) } throw error @@ -46,32 +91,41 @@ export class Ledger { } connect = async () => { - const transport = await TransportNodeHid.create(3000) + if (this.app || this.isConnecting) { + return + } - transport.on('disconnect', async () => { - await transport.close() - this.app = undefined - }) + this.isConnecting = true - if (transport.deviceModel) { - this.logger.debug(`${transport.deviceModel.productName} found.`) - } + let transport: Transport | undefined = undefined - const app = new IronfishApp(transport, this.isMultisig) + try { + transport = await TransportNodeHid.create(2000, 2000) - // If the app isn't open or the device is locked, this will throw an error. - await app.getVersion() + transport.on('disconnect', async () => { + await transport?.close() + this.app = undefined + }) - this.app = app + if (transport.deviceModel) { + this.logger.debug(`${transport.deviceModel.productName} found.`) + } - return { app, PATH: this.PATH } - } + const app = new IronfishApp(transport, this.isMultisig) - protected refreshConnection = async () => { - if (!this.app) { - await this.connect() + this.app = app + return { app, PATH: this.PATH } + } catch (e) { + await transport?.close() + throw e + } finally { + this.isConnecting = false } } + + close = () => { + void this.app?.transport.close() + } } export function isResponseAddress(response: KeyResponse): response is ResponseAddress { @@ -86,28 +140,29 @@ export function isResponseProofGenKey(response: KeyResponse): response is Respon return 'ak' in response && 'nsk' in response } -export function isResponseError(error: unknown): error is ResponseError { - return 'errorMessage' in (error as object) && 'returnCode' in (error as object) -} - export class LedgerError extends Error { name = this.constructor.name } -export class LedgerDeviceLockedError extends LedgerError { - static returnCode = 0x5515 +export class LedgerConnectError extends LedgerError { + static IsError(error: unknown): error is Error { + return ( + error instanceof Error && + 'id' in error && + typeof error['id'] === 'string' && + error.id === 'ListenTimeout' + ) + } } -export class LedgerAppUnavailableError extends LedgerError { - static returnCodes = [ - 0x6d00, // Instruction not supported - 0xffff, // Unknown transport error - 0x6f00, // Technical error - ] - - constructor() { - super( - `Unable to connect to Ironfish app on Ledger. Please check that the device is unlocked and the app is open.`, - ) +export class LedgerPortIsBusyError extends LedgerError { + static IsError(error: unknown): error is Error { + return error instanceof Error && error.message.includes('cannot open device with path') } } + +export class LedgerDeviceLockedError extends LedgerError {} +export class LedgerAppLocked extends LedgerError {} +export class LedgerGPAuthFailed extends LedgerError {} +export class LedgerClaNotSupportedError extends LedgerError {} +export class LedgerAppNotOpen extends LedgerError {} diff --git a/ironfish-cli/src/ledger/ledgerMultiSigner.ts b/ironfish-cli/src/ledger/ledgerMultiSigner.ts index a90c8441ed..87489024e7 100644 --- a/ironfish-cli/src/ledger/ledgerMultiSigner.ts +++ b/ironfish-cli/src/ledger/ledgerMultiSigner.ts @@ -7,7 +7,6 @@ import { KeyResponse, ResponseDkgRound1, ResponseDkgRound2, - ResponseIdentity, } from '@zondax/ledger-ironfish' import { isResponseAddress, isResponseProofGenKey, isResponseViewKey, Ledger } from './ledger' @@ -17,11 +16,9 @@ export class LedgerMultiSigner extends Ledger { } dkgGetIdentity = async (index: number): Promise => { - this.logger.log('Retrieving identity from ledger device.') + this.logger.debug('Retrieving identity from ledger device.') - const response: ResponseIdentity = await this.tryInstruction((app) => - app.dkgGetIdentity(index, false), - ) + const response = await this.tryInstruction((app) => app.dkgGetIdentity(index, false)) return response.identity } diff --git a/ironfish-cli/src/ui/index.ts b/ironfish-cli/src/ui/index.ts index 25752ab000..b010b28865 100644 --- a/ironfish-cli/src/ui/index.ts +++ b/ironfish-cli/src/ui/index.ts @@ -11,3 +11,4 @@ export * from './prompts' export * from './retry' export * from './table' export * from './wallet' +export * from './ledger' diff --git a/ironfish-cli/src/ui/ledger.ts b/ironfish-cli/src/ui/ledger.ts new file mode 100644 index 0000000000..7465e1fafd --- /dev/null +++ b/ironfish-cli/src/ui/ledger.ts @@ -0,0 +1,103 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { PromiseUtils } from '@ironfish/sdk' +import { ux } from '@oclif/core' +import inquirer from 'inquirer' +import { + Ledger, + LedgerAppLocked, + LedgerAppNotOpen, + LedgerClaNotSupportedError, + LedgerConnectError, + LedgerDeviceLockedError, + LedgerGPAuthFailed, + LedgerPortIsBusyError, +} from '../ledger' + +export async function ledger({ + ledger, + action, + message = 'Ledger', + approval, +}: { + ledger: Ledger + action: () => TResult | Promise + message?: string + approval?: boolean +}): Promise { + const wasRunning = ux.action.running + let statusAdded = false + + if (approval) { + message = `Approve ${message}` + } + + if (!wasRunning) { + ux.action.start(message) + } + + try { + // eslint-disable-next-line no-constant-condition + while (true) { + try { + const result = await action() + ux.action.stop() + return result + } catch (e) { + if (e instanceof LedgerAppLocked) { + // If an app is running and it's locked, trying to poll the device + // will cause the Ledger device to hide the pin screen as the user + // is trying to enter their pin. When we run into this error, we + // cannot send any commands to the Ledger in the app's CLA. + ux.action.stop('Ledger App Locked') + + await inquirer.prompt<{ retry: boolean }>([ + { + name: 'retry', + message: `Ledger App Locked. Unlock and press enter to retry:`, + type: 'list', + choices: [ + { + name: `Retry`, + value: true, + default: true, + }, + ], + }, + ]) + + if (!wasRunning) { + ux.action.start(message) + } + } else if (e instanceof LedgerConnectError) { + ux.action.status = 'Connect and unlock your Ledger' + } else if (e instanceof LedgerAppNotOpen) { + const appName = ledger.isMultisig ? 'Ironfish DKG' : 'Ironfish' + ux.action.status = `Open Ledger App ${appName}` + } else if (e instanceof LedgerDeviceLockedError) { + ux.action.status = 'Unlock Ledger' + } else if (e instanceof LedgerPortIsBusyError) { + ux.action.status = 'Ledger is busy, retrying' + } else if (e instanceof LedgerGPAuthFailed) { + ux.action.status = 'Ledger handshake failed, retrying' + } else if (e instanceof LedgerClaNotSupportedError) { + const appName = ledger.isMultisig ? 'Ironfish DKG' : 'Ironfish' + ux.action.status = `Wrong Ledger app. Please open ${appName}` + } else { + throw e + } + + statusAdded = true + await PromiseUtils.sleep(1000) + continue + } + } + } finally { + // Don't interrupt an existing status outside of ledgerAction() + if (!wasRunning && statusAdded) { + ux.action.stop() + } + } +} From 5cd8120432d0a4aa166a2314e1a1175bc76cabeb Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Mon, 7 Oct 2024 08:32:04 -0700 Subject: [PATCH 31/73] adds idle session timeout to multisig broker server (#5485) uses a timeout to clean up data from idle sessions instead of cleaning up immediately after last client disconnects allows for clients to reconnect to sessions adds cli flag to server command --- .../src/commands/wallet/multisig/server.ts | 9 ++++++- ironfish-cli/src/multisigBroker/server.ts | 25 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/server.ts b/ironfish-cli/src/commands/wallet/multisig/server.ts index 2c67067822..1ad674f596 100644 --- a/ironfish-cli/src/commands/wallet/multisig/server.ts +++ b/ironfish-cli/src/commands/wallet/multisig/server.ts @@ -29,12 +29,19 @@ export class MultisigServerCommand extends IronfishCommand { allowNo: true, default: true, }), + idleSessionTimeout: Flags.integer({ + description: 'time (in ms) to wait before cleaning up idle session data', + char: 'i', + }), } async start(): Promise { const { flags } = await this.parse(MultisigServerCommand) - const server = new MultisigServer({ logger: this.logger }) + const server = new MultisigServer({ + logger: this.logger, + idleSessionTimeout: flags.idleSessionTimeout, + }) let adapter: IMultisigBrokerAdapter if (flags.tls) { diff --git a/ironfish-cli/src/multisigBroker/server.ts b/ironfish-cli/src/multisigBroker/server.ts index 2c1d9564f7..cfc3be382f 100644 --- a/ironfish-cli/src/multisigBroker/server.ts +++ b/ironfish-cli/src/multisigBroker/server.ts @@ -37,6 +37,7 @@ interface MultisigSession { clientIds: Set status: DkgStatus | SigningStatus challenge: string + timeout: NodeJS.Timeout | undefined } interface DkgSession extends MultisigSession { @@ -77,13 +78,15 @@ export class MultisigServer { private _isRunning = false private _startPromise: Promise | null = null + private idleSessionTimeout: number - constructor(options: { logger: Logger; banning?: boolean }) { + constructor(options: { logger: Logger; idleSessionTimeout?: number }) { this.logger = options.logger this.clients = new Map() this.nextClientId = 1 this.nextMessageId = 1 + this.idleSessionTimeout = options.idleSessionTimeout ?? 600000 } get isRunning(): boolean { @@ -111,6 +114,10 @@ export class MultisigServer { await this._startPromise } + for (const session of this.sessions.values()) { + clearTimeout(session.timeout) + } + await Promise.all(this.adapters.map((a) => a.stop())) this._isRunning = false } @@ -163,7 +170,7 @@ export class MultisigServer { this.removeClientFromSession(client) if (!this.isSessionActive(sessionId)) { - this.cleanupSession(sessionId) + this.setSessionTimeout(sessionId) } } } @@ -256,6 +263,15 @@ export class MultisigServer { return false } + private setSessionTimeout(sessionId: string): void { + const session = this.sessions.get(sessionId) + if (!session) { + return + } + + session.timeout = setTimeout(() => this.cleanupSession(sessionId), this.idleSessionTimeout) + } + private cleanupSession(sessionId: string): void { this.sessions.delete(sessionId) this.logger.debug(`Session ${sessionId} cleaned up. Active sessions: ${this.sessions.size}`) @@ -269,6 +285,9 @@ export class MultisigServer { client.sessionId = session.id session.clientIds.add(client.id) + + clearTimeout(session.timeout) + session.timeout = undefined } private removeClientFromSession(client: MultisigServerClient): void { @@ -418,6 +437,7 @@ export class MultisigServer { round2PublicPackages: [], }, challenge: body.result.challenge, + timeout: undefined, } this.sessions.set(sessionId, session) @@ -456,6 +476,7 @@ export class MultisigServer { signatureShares: [], }, challenge: body.result.challenge, + timeout: undefined, } this.sessions.set(sessionId, session) From 72ac760611b288421cbd971b20fe25d64f4c6ca5 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 7 Oct 2024 10:22:57 -0700 Subject: [PATCH 32/73] Add @ledgerhq/errors package to package.json (#5487) We use this in our ledger integration --- ironfish-cli/package.json | 1 + yarn.lock | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index d5cae5ea67..a42d2fdff1 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -62,6 +62,7 @@ "dependencies": { "@ironfish/rust-nodejs": "2.7.0", "@ironfish/sdk": "2.7.0", + "@ledgerhq/errors": "6.19.1", "@ledgerhq/hw-transport-node-hid": "6.29.1", "@oclif/core": "4.0.11", "@oclif/plugin-help": "6.2.5", diff --git a/yarn.lock b/yarn.lock index 289ef0150b..39a2a92a4d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1515,6 +1515,11 @@ rxjs "^7.8.1" semver "^7.3.5" +"@ledgerhq/errors@6.19.1": + version "6.19.1" + resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.19.1.tgz#d9ac45ad4ff839e468b8f63766e665537aaede58" + integrity sha512-75yK7Nnit/Gp7gdrJAz0ipp31CCgncRp+evWt6QawQEtQKYEDfGo10QywgrrBBixeRxwnMy1DP6g2oCWRf1bjw== + "@ledgerhq/errors@^6.17.0": version "6.17.0" resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.17.0.tgz#0d56361fe6eb7de3b239e661710679f933f1fcca" From d8c9965bea0ebbce9bac4605f2556aac6910f124 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 7 Oct 2024 10:51:41 -0700 Subject: [PATCH 33/73] Upgrade wallet:multisig:sign to use ui.ledger (#5489) * Upgrade wallet:multisig:sign to use ui.ledger This uses the new ledger UI action so that it will wait for the user to connect and unlock their ledger. * Update ironfish-cli/src/commands/wallet/multisig/sign.ts Co-authored-by: Hugh Cunningham <57735705+hughy@users.noreply.github.com> --------- Co-authored-by: Hugh Cunningham <57735705+hughy@users.noreply.github.com> --- .../src/commands/wallet/multisig/sign.ts | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts index 938f273964..4ca047b623 100644 --- a/ironfish-cli/src/commands/wallet/multisig/sign.ts +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -73,15 +73,6 @@ export class SignMultisigTransactionCommand extends IronfishCommand { if (flags.ledger) { ledger = new LedgerMultiSigner(this.logger) - try { - await ledger.connect() - } catch (e) { - if (e instanceof Error) { - this.error(e.message) - } else { - throw e - } - } } let multisigAccountName: string @@ -387,11 +378,16 @@ export class SignMultisigTransactionCommand extends IronfishCommand { const signingPackage = new multisig.SigningPackage(Buffer.from(signingPackageString, 'hex')) if (ledger) { - const frostSignatureShare = await ledger.dkgSign( - unsignedTransaction.publicKeyRandomness(), - signingPackage.frostSigningPackage().toString('hex'), - unsignedTransaction.hash().toString('hex'), - ) + const frostSignatureShare = await ui.ledger({ + ledger, + message: 'Sign Transaction', + action: () => + ledger.dkgSign( + unsignedTransaction.publicKeyRandomness(), + signingPackage.frostSigningPackage().toString('hex'), + unsignedTransaction.hash().toString('hex'), + ), + }) signatureShare = multisig.SignatureShare.fromFrost( frostSignatureShare, @@ -515,7 +511,12 @@ export class SignMultisigTransactionCommand extends IronfishCommand { let commitment if (ledger) { - await ledger.reviewTransaction(unsignedTransactionHex) + await ui.ledger({ + ledger, + message: 'Review Transaction', + action: () => ledger.reviewTransaction(unsignedTransactionHex), + approval: true, + }) commitment = await this.createSigningCommitmentWithLedger( ledger, @@ -545,7 +546,11 @@ export class SignMultisigTransactionCommand extends IronfishCommand { transactionHash: Buffer, signers: string[], ): Promise { - const rawCommitments = await ledger.dkgGetCommitments(transactionHash.toString('hex')) + const rawCommitments = await ui.ledger({ + ledger, + message: 'Get Commitments', + action: () => ledger.dkgGetCommitments(transactionHash.toString('hex')), + }) const sigingCommitment = multisig.SigningCommitment.fromRaw( participant.identity, From bf4f474079e9e07c5dc47cee6219226580f70872 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 7 Oct 2024 10:51:53 -0700 Subject: [PATCH 34/73] Move ledger UI functions into UI (#5486) This makes the ledger module platform independent --- .../src/commands/wallet/chainport/send.ts | 3 +- ironfish-cli/src/commands/wallet/mint.ts | 3 +- ironfish-cli/src/commands/wallet/send.ts | 3 +- ironfish-cli/src/ledger/index.ts | 1 - ironfish-cli/src/ledger/ui.ts | 98 ------------------- ironfish-cli/src/ui/ledger.ts | 98 ++++++++++++++++++- 6 files changed, 99 insertions(+), 107 deletions(-) delete mode 100644 ironfish-cli/src/ledger/ui.ts diff --git a/ironfish-cli/src/commands/wallet/chainport/send.ts b/ironfish-cli/src/commands/wallet/chainport/send.ts index 51c89f3c35..1612feeb79 100644 --- a/ironfish-cli/src/commands/wallet/chainport/send.ts +++ b/ironfish-cli/src/commands/wallet/chainport/send.ts @@ -17,7 +17,6 @@ import { Flags, ux } from '@oclif/core' import inquirer from 'inquirer' import { IronfishCommand } from '../../../command' import { HexFlag, IronFlag, RemoteFlags, ValueFlag } from '../../../flags' -import { sendTransactionWithLedger } from '../../../ledger' import * as ui from '../../../ui' import { ChainportBridgeTransaction, @@ -135,7 +134,7 @@ export class BridgeCommand extends IronfishCommand { } if (flags.ledger) { - await sendTransactionWithLedger( + await ui.sendTransactionWithLedger( client, rawTransaction, from, diff --git a/ironfish-cli/src/commands/wallet/mint.ts b/ironfish-cli/src/commands/wallet/mint.ts index 57b4868eed..d2b5d8a294 100644 --- a/ironfish-cli/src/commands/wallet/mint.ts +++ b/ironfish-cli/src/commands/wallet/mint.ts @@ -17,7 +17,6 @@ import { import { Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../command' import { IronFlag, RemoteFlags, ValueFlag } from '../../flags' -import { sendTransactionWithLedger } from '../../ledger' import * as ui from '../../ui' import { useAccount } from '../../utils' import { promptCurrency } from '../../utils/currency' @@ -314,7 +313,7 @@ This will create tokens and increase supply for a given asset.` ) if (flags.ledger) { - await sendTransactionWithLedger( + await ui.sendTransactionWithLedger( client, raw, account, diff --git a/ironfish-cli/src/commands/wallet/send.ts b/ironfish-cli/src/commands/wallet/send.ts index c76da5898f..50d3875b89 100644 --- a/ironfish-cli/src/commands/wallet/send.ts +++ b/ironfish-cli/src/commands/wallet/send.ts @@ -14,7 +14,6 @@ import { import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { HexFlag, IronFlag, RemoteFlags, ValueFlag } from '../../flags' -import { sendTransactionWithLedger } from '../../ledger' import * as ui from '../../ui' import { useAccount } from '../../utils' import { promptCurrency } from '../../utils/currency' @@ -258,7 +257,7 @@ export class Send extends IronfishCommand { } if (flags.ledger) { - await sendTransactionWithLedger( + await ui.sendTransactionWithLedger( client, raw, from, diff --git a/ironfish-cli/src/ledger/index.ts b/ironfish-cli/src/ledger/index.ts index 4de4c815f8..3fe51ee586 100644 --- a/ironfish-cli/src/ledger/index.ts +++ b/ironfish-cli/src/ledger/index.ts @@ -4,4 +4,3 @@ export * from './ledger' export * from './ledgerMultiSigner' export * from './ledgerSingleSigner' -export * from './ui' diff --git a/ironfish-cli/src/ledger/ui.ts b/ironfish-cli/src/ledger/ui.ts deleted file mode 100644 index c37567877b..0000000000 --- a/ironfish-cli/src/ledger/ui.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { - CurrencyUtils, - Logger, - RawTransaction, - RawTransactionSerde, - RpcClient, - Transaction, -} from '@ironfish/sdk' -import { Errors, ux } from '@oclif/core' -import * as ui from '../ui' -import { watchTransaction } from '../utils/transaction' -import { LedgerSingleSigner } from './ledgerSingleSigner' - -export async function sendTransactionWithLedger( - client: RpcClient, - raw: RawTransaction, - from: string | undefined, - watch: boolean, - confirm: boolean, - logger?: Logger, -): Promise { - const ledger = new LedgerSingleSigner(logger) - - try { - await ledger.connect() - } catch (e) { - if (e instanceof Error) { - Errors.error(e.message) - } else { - throw e - } - } - - const publicKey = (await client.wallet.getAccountPublicKey({ account: from })).content - .publicKey - - const ledgerPublicKey = await ledger.getPublicAddress() - - if (publicKey !== ledgerPublicKey) { - Errors.error( - `The public key on the ledger device does not match the public key of the account '${from}'`, - ) - } - - const buildTransactionResponse = await client.wallet.buildTransaction({ - account: from, - rawTransaction: RawTransactionSerde.serialize(raw).toString('hex'), - }) - - const unsignedTransaction = buildTransactionResponse.content.unsignedTransaction - - ux.stdout('Please confirm the transaction on your Ledger device') - - const signature = (await ledger.sign(unsignedTransaction)).toString('hex') - - ux.stdout(`\nSignature: ${signature}`) - - const addSignatureResponse = await client.wallet.addSignature({ - unsignedTransaction, - signature, - }) - - const signedTransaction = addSignatureResponse.content.transaction - const bytes = Buffer.from(signedTransaction, 'hex') - - const transaction = new Transaction(bytes) - - ux.stdout(`\nSigned Transaction: ${signedTransaction}`) - ux.stdout(`\nHash: ${transaction.hash().toString('hex')}`) - ux.stdout(`Fee: ${CurrencyUtils.render(transaction.fee(), true)}`) - - await ui.confirmOrQuit('Would you like to broadcast this transaction?', confirm) - - const addTransactionResponse = await client.wallet.addTransaction({ - transaction: signedTransaction, - broadcast: true, - }) - - if (addTransactionResponse.content.accepted === false) { - Errors.error( - `Transaction '${transaction.hash().toString('hex')}' was not accepted into the mempool`, - ) - } - - if (watch) { - ux.stdout('') - - await watchTransaction({ - client, - logger, - account: from, - hash: transaction.hash().toString('hex'), - }) - } -} diff --git a/ironfish-cli/src/ui/ledger.ts b/ironfish-cli/src/ui/ledger.ts index 7465e1fafd..ff0923d294 100644 --- a/ironfish-cli/src/ui/ledger.ts +++ b/ironfish-cli/src/ui/ledger.ts @@ -2,8 +2,16 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { PromiseUtils } from '@ironfish/sdk' -import { ux } from '@oclif/core' +import { + CurrencyUtils, + Logger, + PromiseUtils, + RawTransaction, + RawTransactionSerde, + RpcClient, + Transaction, +} from '@ironfish/sdk' +import { Errors, ux } from '@oclif/core' import inquirer from 'inquirer' import { Ledger, @@ -14,7 +22,10 @@ import { LedgerDeviceLockedError, LedgerGPAuthFailed, LedgerPortIsBusyError, + LedgerSingleSigner, } from '../ledger' +import * as ui from '../ui' +import { watchTransaction } from '../utils/transaction' export async function ledger({ ledger, @@ -101,3 +112,86 @@ export async function ledger({ } } } + +export async function sendTransactionWithLedger( + client: RpcClient, + raw: RawTransaction, + from: string | undefined, + watch: boolean, + confirm: boolean, + logger?: Logger, +): Promise { + const ledger = new LedgerSingleSigner(logger) + + try { + await ledger.connect() + } catch (e) { + if (e instanceof Error) { + Errors.error(e.message) + } else { + throw e + } + } + + const publicKey = (await client.wallet.getAccountPublicKey({ account: from })).content + .publicKey + + const ledgerPublicKey = await ledger.getPublicAddress() + + if (publicKey !== ledgerPublicKey) { + Errors.error( + `The public key on the ledger device does not match the public key of the account '${from}'`, + ) + } + + const buildTransactionResponse = await client.wallet.buildTransaction({ + account: from, + rawTransaction: RawTransactionSerde.serialize(raw).toString('hex'), + }) + + const unsignedTransaction = buildTransactionResponse.content.unsignedTransaction + + ux.stdout('Please confirm the transaction on your Ledger device') + + const signature = (await ledger.sign(unsignedTransaction)).toString('hex') + + ux.stdout(`\nSignature: ${signature}`) + + const addSignatureResponse = await client.wallet.addSignature({ + unsignedTransaction, + signature, + }) + + const signedTransaction = addSignatureResponse.content.transaction + const bytes = Buffer.from(signedTransaction, 'hex') + + const transaction = new Transaction(bytes) + + ux.stdout(`\nSigned Transaction: ${signedTransaction}`) + ux.stdout(`\nHash: ${transaction.hash().toString('hex')}`) + ux.stdout(`Fee: ${CurrencyUtils.render(transaction.fee(), true)}`) + + await ui.confirmOrQuit('Would you like to broadcast this transaction?', confirm) + + const addTransactionResponse = await client.wallet.addTransaction({ + transaction: signedTransaction, + broadcast: true, + }) + + if (addTransactionResponse.content.accepted === false) { + Errors.error( + `Transaction '${transaction.hash().toString('hex')}' was not accepted into the mempool`, + ) + } + + if (watch) { + ux.stdout('') + + await watchTransaction({ + client, + logger, + account: from, + hash: transaction.hash().toString('hex'), + }) + } +} From ed2af01582e7741cf57a45294f698a2f7804e94b Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 7 Oct 2024 10:52:05 -0700 Subject: [PATCH 35/73] Upgrade wallet:import to use ui.ledger (#5488) --- ironfish-cli/src/commands/wallet/import.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/import.ts b/ironfish-cli/src/commands/wallet/import.ts index 166653d4c8..b23086f029 100644 --- a/ironfish-cli/src/commands/wallet/import.ts +++ b/ironfish-cli/src/commands/wallet/import.ts @@ -7,6 +7,7 @@ import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' import { LedgerError, LedgerSingleSigner } from '../../ledger' import { checkWalletUnlocked, inputPrompt } from '../../ui' +import * as ui from '../../ui' import { importFile, importPipe, longPrompt } from '../../ui/longPrompt' import { importAccount } from '../../utils' @@ -119,8 +120,14 @@ export class ImportCommand extends IronfishCommand { async importLedger(): Promise { try { const ledger = new LedgerSingleSigner(this.logger) - await ledger.connect() - const account = await ledger.importAccount() + + const account = await ui.ledger({ + ledger, + message: 'Import Wallet', + approval: true, + action: () => ledger.importAccount(), + }) + return encodeAccountImport(account, AccountFormat.Base64Json) } catch (e) { if (e instanceof LedgerError) { From fa624277fa907d07b169551d01bce6c7422cc706 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 7 Oct 2024 12:18:01 -0700 Subject: [PATCH 36/73] Handle appInfo() crash on rust app (#5491) --- ironfish-cli/src/ledger/ledger.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/ironfish-cli/src/ledger/ledger.ts b/ironfish-cli/src/ledger/ledger.ts index 53b71aeddb..e5a0ea8e5a 100644 --- a/ironfish-cli/src/ledger/ledger.ts +++ b/ironfish-cli/src/ledger/ledger.ts @@ -33,7 +33,18 @@ export class Ledger { // App info is a request to the dashboard CLA. The purpose of this it to // produce a Locked Device error and works if an app is open or closed. - await this.app.appInfo() + try { + await this.app.appInfo() + } catch (error) { + if ( + error instanceof ResponseError && + error.message.includes('Attempt to read beyond buffer length') && + error.returnCode === LedgerStatusCodes.TECHNICAL_PROBLEM + ) { + // Catch this error and swollow it until the SDK fix merges to fix + // this + } + } // This is an app specific request. This is useful because this throws // INS_NOT_SUPPORTED in the case that the app is locked which is useful to From 5253783d6ef20053d53f5e0894da3d7f66f0c990 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 7 Oct 2024 12:33:02 -0700 Subject: [PATCH 37/73] Use the connected app in case of disconnect (#5492) Since we can disconnect on the appInfo() call we need to cache this and make sure we use the correct app. --- ironfish-cli/src/ledger/ledger.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ironfish-cli/src/ledger/ledger.ts b/ironfish-cli/src/ledger/ledger.ts index e5a0ea8e5a..69c8fca9a6 100644 --- a/ironfish-cli/src/ledger/ledger.ts +++ b/ironfish-cli/src/ledger/ledger.ts @@ -31,10 +31,12 @@ export class Ledger { Assert.isNotUndefined(this.app, 'Unable to establish connection with Ledger device') + const app = this.app + // App info is a request to the dashboard CLA. The purpose of this it to // produce a Locked Device error and works if an app is open or closed. try { - await this.app.appInfo() + await app.appInfo() } catch (error) { if ( error instanceof ResponseError && @@ -50,7 +52,7 @@ export class Ledger { // INS_NOT_SUPPORTED in the case that the app is locked which is useful to // know versus the device is locked. try { - await this.app.getVersion() + await app.getVersion() } catch (error) { if ( error instanceof ResponseError && @@ -62,7 +64,7 @@ export class Ledger { throw error } - return await instruction(this.app) + return await instruction(app) } catch (error: unknown) { if (LedgerPortIsBusyError.IsError(error)) { throw new LedgerPortIsBusyError() From df1f22c0ebf52e5dd052aed38ba599f7a0e502bd Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:01:12 -0700 Subject: [PATCH 38/73] get identity after dkg parameters in dkg:create (#5495) when starting a new dkg session with a ledger the user currently sees output about getting the identity from the ledger in between choosing to start a new session and entering the max/min signers for that session --- .../src/commands/wallet/multisig/dkg/create.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index 91455a0cd1..4ec08c4c69 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -121,12 +121,6 @@ export class DkgCreateCommand extends IronfishCommand { } } - const { name: participantName, identity } = await this.getOrCreateIdentity( - client, - ledger, - accountName, - ) - const { totalParticipants, minSigners } = await ui.retryStep( async () => { return this.getDkgConfig(multisigClient, !!ledger) @@ -135,6 +129,12 @@ export class DkgCreateCommand extends IronfishCommand { true, ) + const { name: participantName, identity } = await this.getOrCreateIdentity( + client, + ledger, + accountName, + ) + const { round1 } = await ui.retryStep( async () => { return this.performRound1( From 7958f26cc316ea3edf69e3f493edbd95095213c0 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:22:31 -0700 Subject: [PATCH 39/73] resets ledger ui status between errors (#5490) * resets ledger ui status between errors removes the error status (e.g., 'Open Ledger App Ironfish') after the user has resolved the cause of the error * uses timer to clear status ledger ux action status sets a timer to clear the status instead of clearing on each iteration. prevents error status message from flashing if the user has not fixed the cause between iterations --- ironfish-cli/src/ui/ledger.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ironfish-cli/src/ui/ledger.ts b/ironfish-cli/src/ui/ledger.ts index ff0923d294..5740ebb139 100644 --- a/ironfish-cli/src/ui/ledger.ts +++ b/ironfish-cli/src/ui/ledger.ts @@ -49,6 +49,8 @@ export async function ledger({ ux.action.start(message) } + let clearStatusTimer + try { // eslint-disable-next-line no-constant-condition while (true) { @@ -57,6 +59,7 @@ export async function ledger({ ux.action.stop() return result } catch (e) { + clearTimeout(clearStatusTimer) if (e instanceof LedgerAppLocked) { // If an app is running and it's locked, trying to poll the device // will cause the Ledger device to hide the pin screen as the user @@ -101,6 +104,7 @@ export async function ledger({ } statusAdded = true + clearStatusTimer = setTimeout(() => (ux.action.status = undefined), 2000) await PromiseUtils.sleep(1000) continue } @@ -108,6 +112,7 @@ export async function ledger({ } finally { // Don't interrupt an existing status outside of ledgerAction() if (!wasRunning && statusAdded) { + clearTimeout(clearStatusTimer) ux.action.stop() } } From 2424713a2fa756d5e324cd69b780e73717903f05 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 7 Oct 2024 16:24:26 -0700 Subject: [PATCH 40/73] Use the same ledgerhq/errors as other lib (#5498) --- ironfish-cli/package.json | 2 +- yarn.lock | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index a42d2fdff1..6e11745999 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -62,7 +62,7 @@ "dependencies": { "@ironfish/rust-nodejs": "2.7.0", "@ironfish/sdk": "2.7.0", - "@ledgerhq/errors": "6.19.1", + "@ledgerhq/errors": "6.17.0", "@ledgerhq/hw-transport-node-hid": "6.29.1", "@oclif/core": "4.0.11", "@oclif/plugin-help": "6.2.5", diff --git a/yarn.lock b/yarn.lock index 39a2a92a4d..8f6900b489 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1515,12 +1515,7 @@ rxjs "^7.8.1" semver "^7.3.5" -"@ledgerhq/errors@6.19.1": - version "6.19.1" - resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.19.1.tgz#d9ac45ad4ff839e468b8f63766e665537aaede58" - integrity sha512-75yK7Nnit/Gp7gdrJAz0ipp31CCgncRp+evWt6QawQEtQKYEDfGo10QywgrrBBixeRxwnMy1DP6g2oCWRf1bjw== - -"@ledgerhq/errors@^6.17.0": +"@ledgerhq/errors@6.17.0", "@ledgerhq/errors@^6.17.0": version "6.17.0" resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.17.0.tgz#0d56361fe6eb7de3b239e661710679f933f1fcca" integrity sha512-xnOVpy/gUUkusEORdr2Qhw3Vd0MGfjyVGgkGR9Ck6FXE26OIdIQ3tNmG5BdZN+gwMMFJJVxxS4/hr0taQfZ43w== From edd83ad6cebd2d2c450e1f37c282e5ff79ddcff5 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 7 Oct 2024 16:26:41 -0700 Subject: [PATCH 41/73] Unify ledger errors and zondax errors (#5499) This makes one unified type with all the known error codes. --- ironfish-cli/src/ledger/ledger.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/ironfish-cli/src/ledger/ledger.ts b/ironfish-cli/src/ledger/ledger.ts index 69c8fca9a6..76c1661399 100644 --- a/ironfish-cli/src/ledger/ledger.ts +++ b/ironfish-cli/src/ledger/ledger.ts @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { Assert, createRootLogger, Logger } from '@ironfish/sdk' -import { StatusCodes as LedgerStatusCodes, TransportStatusError } from '@ledgerhq/errors' +import { StatusCodes, TransportStatusError } from '@ledgerhq/errors' import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' import IronfishApp, { KeyResponse, @@ -12,6 +12,11 @@ import IronfishApp, { } from '@zondax/ledger-ironfish' import { ResponseError, Transport } from '@zondax/ledger-js' +export const IronfishLedgerStatusCodes = { + ...StatusCodes, + COMMAND_NOT_ALLOWED: 0x6986, +} + export class Ledger { app: IronfishApp | undefined logger: Logger @@ -41,7 +46,7 @@ export class Ledger { if ( error instanceof ResponseError && error.message.includes('Attempt to read beyond buffer length') && - error.returnCode === LedgerStatusCodes.TECHNICAL_PROBLEM + error.returnCode === IronfishLedgerStatusCodes.TECHNICAL_PROBLEM ) { // Catch this error and swollow it until the SDK fix merges to fix // this @@ -56,7 +61,7 @@ export class Ledger { } catch (error) { if ( error instanceof ResponseError && - error.returnCode === LedgerStatusCodes.INS_NOT_SUPPORTED + error.returnCode === IronfishLedgerStatusCodes.INS_NOT_SUPPORTED ) { throw new LedgerAppLocked() } @@ -77,16 +82,16 @@ export class Ledger { } if (error instanceof ResponseError) { - if (error.returnCode === LedgerStatusCodes.LOCKED_DEVICE) { + if (error.returnCode === IronfishLedgerStatusCodes.LOCKED_DEVICE) { throw new LedgerDeviceLockedError() - } else if (error.returnCode === LedgerStatusCodes.CLA_NOT_SUPPORTED) { + } else if (error.returnCode === IronfishLedgerStatusCodes.CLA_NOT_SUPPORTED) { throw new LedgerClaNotSupportedError() - } else if (error.returnCode === LedgerStatusCodes.GP_AUTH_FAILED) { + } else if (error.returnCode === IronfishLedgerStatusCodes.GP_AUTH_FAILED) { throw new LedgerGPAuthFailed() } else if ( [ - LedgerStatusCodes.INS_NOT_SUPPORTED, - LedgerStatusCodes.TECHNICAL_PROBLEM, + IronfishLedgerStatusCodes.INS_NOT_SUPPORTED, + IronfishLedgerStatusCodes.TECHNICAL_PROBLEM, 0xffff, // Unknown transport error 0x6e01, // App not open ].includes(error.returnCode) From bcafa959120431b12d714cf899c4b2841700b20f Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:30:58 -0700 Subject: [PATCH 42/73] Add flags for minSigners and totalParticipants for dkg:create (#5497) * Add flags for minSigners and totalParticipants for dkg:create * don't prompt for session id if total participants or max signers flags present --- .../commands/wallet/multisig/dkg/create.ts | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index 4ec08c4c69..ee0441328e 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -60,6 +60,14 @@ export class DkgCreateCommand extends IronfishCommand { dependsOn: ['server'], allowNo: true, }), + minSigners: Flags.integer({ + description: 'Minimum signers required to sign a transaction', + exclusive: ['sessionId'], + }), + totalParticipants: Flags.integer({ + description: 'The total number of participants for the multisig account', + exclusive: ['sessionId'], + }), } async start(): Promise { @@ -84,7 +92,7 @@ export class DkgCreateCommand extends IronfishCommand { let multisigClient: MultisigClient | null = null if (flags.server) { let sessionId = flags.sessionId - if (!sessionId) { + if (!sessionId && !flags.totalParticipants && !flags.minSigners) { sessionId = await ui.inputPrompt( 'Enter the ID of a multisig session to join, or press enter to start a new session', false, @@ -123,7 +131,12 @@ export class DkgCreateCommand extends IronfishCommand { const { totalParticipants, minSigners } = await ui.retryStep( async () => { - return this.getDkgConfig(multisigClient, !!ledger) + return this.getDkgConfig( + multisigClient, + !!ledger, + flags.minSigners, + flags.totalParticipants, + ) }, this.logger, true, @@ -263,6 +276,8 @@ export class DkgCreateCommand extends IronfishCommand { async getDkgConfig( multisigClient: MultisigClient | null, ledger: boolean, + minSigners?: number, + totalParticipants?: number, ): Promise<{ totalParticipants: number; minSigners: number }> { if (multisigClient?.sessionId) { let totalParticipants = 0 @@ -285,11 +300,13 @@ export class DkgCreateCommand extends IronfishCommand { return { totalParticipants, minSigners } } - const totalParticipants = await ui.inputNumberPrompt( - this.logger, - 'Enter the total number of participants', - { required: true, integer: true }, - ) + if (!totalParticipants) { + totalParticipants = await ui.inputNumberPrompt( + this.logger, + 'Enter the total number of participants', + { required: true, integer: true }, + ) + } if (totalParticipants < 2) { throw new Error('Total number of participants must be at least 2') @@ -299,11 +316,13 @@ export class DkgCreateCommand extends IronfishCommand { throw new Error('DKG with Ledger supports a maximum of 4 participants') } - const minSigners = await ui.inputNumberPrompt( - this.logger, - 'Enter the number of minimum signers', - { required: true, integer: true }, - ) + if (!minSigners) { + minSigners = await ui.inputNumberPrompt( + this.logger, + 'Enter the number of minimum signers', + { required: true, integer: true }, + ) + } if (minSigners < 2 || minSigners > totalParticipants) { throw new Error( From 6a4bcc12a8276037589790ec01355c96b16c0da9 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 7 Oct 2024 16:41:26 -0700 Subject: [PATCH 43/73] Handle app command rejections (#5500) --- ironfish-cli/src/ledger/ledger.ts | 7 ++++++- ironfish-cli/src/ui/ledger.ts | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/ironfish-cli/src/ledger/ledger.ts b/ironfish-cli/src/ledger/ledger.ts index 76c1661399..d9fb6fa9bb 100644 --- a/ironfish-cli/src/ledger/ledger.ts +++ b/ironfish-cli/src/ledger/ledger.ts @@ -78,7 +78,11 @@ export class Ledger { } if (error instanceof TransportStatusError) { - throw new LedgerConnectError() + if (error.statusCode === IronfishLedgerStatusCodes.COMMAND_NOT_ALLOWED) { + throw new LedgerActionRejected() + } else { + throw new LedgerConnectError() + } } if (error instanceof ResponseError) { @@ -184,3 +188,4 @@ export class LedgerAppLocked extends LedgerError {} export class LedgerGPAuthFailed extends LedgerError {} export class LedgerClaNotSupportedError extends LedgerError {} export class LedgerAppNotOpen extends LedgerError {} +export class LedgerActionRejected extends LedgerError {} diff --git a/ironfish-cli/src/ui/ledger.ts b/ironfish-cli/src/ui/ledger.ts index 5740ebb139..924c359976 100644 --- a/ironfish-cli/src/ui/ledger.ts +++ b/ironfish-cli/src/ui/ledger.ts @@ -15,6 +15,7 @@ import { Errors, ux } from '@oclif/core' import inquirer from 'inquirer' import { Ledger, + LedgerActionRejected, LedgerAppLocked, LedgerAppNotOpen, LedgerClaNotSupportedError, @@ -60,6 +61,7 @@ export async function ledger({ return result } catch (e) { clearTimeout(clearStatusTimer) + if (e instanceof LedgerAppLocked) { // If an app is running and it's locked, trying to poll the device // will cause the Ledger device to hide the pin screen as the user @@ -85,6 +87,9 @@ export async function ledger({ if (!wasRunning) { ux.action.start(message) } + } else if (e instanceof LedgerActionRejected) { + ux.action.status = 'User Rejected Ledger Request!' + ledger.logger.warn('User Rejected Ledger Request!') } else if (e instanceof LedgerConnectError) { ux.action.status = 'Connect and unlock your Ledger' } else if (e instanceof LedgerAppNotOpen) { From f53d5f50494440434470f8cf6b34e0b5122421fa Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 7 Oct 2024 16:54:42 -0700 Subject: [PATCH 44/73] Switch sendTransaction to use ledger UI (#5496) --- ironfish-cli/src/ui/ledger.ts | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/ironfish-cli/src/ui/ledger.ts b/ironfish-cli/src/ui/ledger.ts index 924c359976..47a0f75091 100644 --- a/ironfish-cli/src/ui/ledger.ts +++ b/ironfish-cli/src/ui/ledger.ts @@ -131,22 +131,16 @@ export async function sendTransactionWithLedger( confirm: boolean, logger?: Logger, ): Promise { - const ledger = new LedgerSingleSigner(logger) - - try { - await ledger.connect() - } catch (e) { - if (e instanceof Error) { - Errors.error(e.message) - } else { - throw e - } - } + const ledgerApp = new LedgerSingleSigner(logger) const publicKey = (await client.wallet.getAccountPublicKey({ account: from })).content .publicKey - const ledgerPublicKey = await ledger.getPublicAddress() + const ledgerPublicKey = await ledger({ + ledger: ledgerApp, + message: 'Get Public Address', + action: () => ledgerApp.getPublicAddress(), + }) if (publicKey !== ledgerPublicKey) { Errors.error( @@ -163,13 +157,18 @@ export async function sendTransactionWithLedger( ux.stdout('Please confirm the transaction on your Ledger device') - const signature = (await ledger.sign(unsignedTransaction)).toString('hex') + const signature = await ledger({ + ledger: ledgerApp, + message: 'Sign Transaction', + approval: true, + action: () => ledgerApp.sign(unsignedTransaction), + }) - ux.stdout(`\nSignature: ${signature}`) + ux.stdout(`\nSignature: ${signature.toString('hex')}`) const addSignatureResponse = await client.wallet.addSignature({ unsignedTransaction, - signature, + signature: signature.toString('hex'), }) const signedTransaction = addSignatureResponse.content.transaction From 0b43e46c554580e23756067935867752250aaca0 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 7 Oct 2024 17:06:19 -0700 Subject: [PATCH 45/73] Have ledger ui tell the user to approve (#5501) Now they know when to look down at their device --- ironfish-cli/src/ui/ledger.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ironfish-cli/src/ui/ledger.ts b/ironfish-cli/src/ui/ledger.ts index 47a0f75091..07d670a7ea 100644 --- a/ironfish-cli/src/ui/ledger.ts +++ b/ironfish-cli/src/ui/ledger.ts @@ -42,10 +42,6 @@ export async function ledger({ const wasRunning = ux.action.running let statusAdded = false - if (approval) { - message = `Approve ${message}` - } - if (!wasRunning) { ux.action.start(message) } @@ -56,6 +52,14 @@ export async function ledger({ // eslint-disable-next-line no-constant-condition while (true) { try { + clearStatusTimer = setTimeout(() => { + if (approval) { + ux.action.status = 'Approve on Ledger' + } else { + ux.action.status = undefined + } + }, 1500) + const result = await action() ux.action.stop() return result @@ -109,7 +113,6 @@ export async function ledger({ } statusAdded = true - clearStatusTimer = setTimeout(() => (ux.action.status = undefined), 2000) await PromiseUtils.sleep(1000) continue } From 01a41681e324f8f100e8d2db3553ebd9d687db07 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 7 Oct 2024 17:08:24 -0700 Subject: [PATCH 46/73] Handle DKG reject message (#5502) --- ironfish-cli/src/ledger/ledger.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ironfish-cli/src/ledger/ledger.ts b/ironfish-cli/src/ledger/ledger.ts index d9fb6fa9bb..d3776e64a7 100644 --- a/ironfish-cli/src/ledger/ledger.ts +++ b/ironfish-cli/src/ledger/ledger.ts @@ -78,7 +78,10 @@ export class Ledger { } if (error instanceof TransportStatusError) { - if (error.statusCode === IronfishLedgerStatusCodes.COMMAND_NOT_ALLOWED) { + if ( + error.statusCode === IronfishLedgerStatusCodes.COMMAND_NOT_ALLOWED || + error.statusCode === IronfishLedgerStatusCodes.CONDITIONS_OF_USE_NOT_SATISFIED + ) { throw new LedgerActionRejected() } else { throw new LedgerConnectError() From 0cade9739356567b1f56cf60d691ebbc33fe91f9 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:24:12 -0700 Subject: [PATCH 47/73] removes logger from Ledger class (#5503) the Ledger class and subclasses do not log messages themselves. clients that want log output must log themselves --- ironfish-cli/src/commands/wallet/import.ts | 2 +- .../wallet/multisig/commitment/create.ts | 2 +- .../commands/wallet/multisig/dkg/create.ts | 2 +- .../commands/wallet/multisig/dkg/round1.ts | 2 +- .../commands/wallet/multisig/dkg/round2.ts | 2 +- .../commands/wallet/multisig/dkg/round3.ts | 2 +- .../commands/wallet/multisig/ledger/backup.ts | 2 +- .../commands/wallet/multisig/ledger/import.ts | 2 +- .../wallet/multisig/ledger/restore.ts | 2 +- .../wallet/multisig/participant/create.ts | 2 +- .../src/commands/wallet/multisig/sign.ts | 2 +- .../wallet/multisig/signature/create.ts | 2 +- .../src/commands/wallet/transactions/sign.ts | 2 +- ironfish-cli/src/ledger/ledger.ts | 10 ++------- ironfish-cli/src/ledger/ledgerMultiSigner.ts | 21 ++----------------- ironfish-cli/src/ledger/ledgerSingleSigner.ts | 8 +++---- ironfish-cli/src/ui/ledger.ts | 4 ++-- 17 files changed, 22 insertions(+), 47 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/import.ts b/ironfish-cli/src/commands/wallet/import.ts index b23086f029..c36303fdf5 100644 --- a/ironfish-cli/src/commands/wallet/import.ts +++ b/ironfish-cli/src/commands/wallet/import.ts @@ -119,7 +119,7 @@ export class ImportCommand extends IronfishCommand { async importLedger(): Promise { try { - const ledger = new LedgerSingleSigner(this.logger) + const ledger = new LedgerSingleSigner() const account = await ui.ledger({ ledger, diff --git a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts index c5f680c3dc..fd2a53181c 100644 --- a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts @@ -124,7 +124,7 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { unsignedTransaction: UnsignedTransaction, signers: string[], ): Promise { - const ledger = new LedgerMultiSigner(this.logger) + const ledger = new LedgerMultiSigner() try { await ledger.connect() } catch (e) { diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index ee0441328e..e82d54f2aa 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -78,7 +78,7 @@ export class DkgCreateCommand extends IronfishCommand { let ledger: LedgerMultiSigner | undefined = undefined if (flags.ledger) { - ledger = new LedgerMultiSigner(this.logger) + ledger = new LedgerMultiSigner() } const accountName = await this.getAccountName(client, flags.name ?? flags.participant) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts index 9295f8551d..488f786e1c 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts @@ -100,7 +100,7 @@ export class DkgRound1Command extends IronfishCommand { identities: string[], minSigners: number, ): Promise { - const ledger = new LedgerMultiSigner(this.logger) + const ledger = new LedgerMultiSigner() try { await ledger.connect() } catch (e) { diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts index 2cbd25909a..a09afdb7aa 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts @@ -97,7 +97,7 @@ export class DkgRound2Command extends IronfishCommand { round1PublicPackages: string[], round1SecretPackage: string, ): Promise { - const ledger = new LedgerMultiSigner(this.logger) + const ledger = new LedgerMultiSigner() try { await ledger.connect() } catch (e) { diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts index 9afb7cd9e6..02541ef461 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -162,7 +162,7 @@ export class DkgRound3Command extends IronfishCommand { round2SecretPackage: string, accountCreatedAt?: number, ): Promise { - const ledger = new LedgerMultiSigner(this.logger) + const ledger = new LedgerMultiSigner() try { await ledger.connect() } catch (e) { diff --git a/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts b/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts index f6b07fb083..0c1e7b4f91 100644 --- a/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts +++ b/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts @@ -8,7 +8,7 @@ export class MultisigLedgerBackup extends IronfishCommand { static description = `show encrypted multisig keys from a Ledger device` async start(): Promise { - const ledger = new LedgerMultiSigner(this.logger) + const ledger = new LedgerMultiSigner() try { await ledger.connect() } catch (e) { diff --git a/ironfish-cli/src/commands/wallet/multisig/ledger/import.ts b/ironfish-cli/src/commands/wallet/multisig/ledger/import.ts index 091589217f..b9199e8c6d 100644 --- a/ironfish-cli/src/commands/wallet/multisig/ledger/import.ts +++ b/ironfish-cli/src/commands/wallet/multisig/ledger/import.ts @@ -31,7 +31,7 @@ export class MultisigLedgerImport extends IronfishCommand { const name = flags.name ?? (await ui.inputPrompt('Enter a name for the account', true)) - const ledger = new LedgerMultiSigner(this.logger) + const ledger = new LedgerMultiSigner() try { await ledger.connect() } catch (e) { diff --git a/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts b/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts index ad1ffa3123..7251c7b8c5 100644 --- a/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts +++ b/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts @@ -26,7 +26,7 @@ export class MultisigLedgerRestore extends IronfishCommand { ) } - const ledger = new LedgerMultiSigner(this.logger) + const ledger = new LedgerMultiSigner() try { await ledger.connect() } catch (e) { diff --git a/ironfish-cli/src/commands/wallet/multisig/participant/create.ts b/ironfish-cli/src/commands/wallet/multisig/participant/create.ts index c02a4d84c5..d9e7df0f66 100644 --- a/ironfish-cli/src/commands/wallet/multisig/participant/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/participant/create.ts @@ -71,7 +71,7 @@ export class MultisigIdentityCreate extends IronfishCommand { } async getIdentityFromLedger(): Promise { - const ledger = new LedgerMultiSigner(this.logger) + const ledger = new LedgerMultiSigner() try { await ledger.connect() } catch (e) { diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts index 4ca047b623..59957487f4 100644 --- a/ironfish-cli/src/commands/wallet/multisig/sign.ts +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -72,7 +72,7 @@ export class SignMultisigTransactionCommand extends IronfishCommand { let ledger: LedgerMultiSigner | undefined = undefined if (flags.ledger) { - ledger = new LedgerMultiSigner(this.logger) + ledger = new LedgerMultiSigner() } let multisigAccountName: string diff --git a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts index 8ef3f6076c..2d487a204c 100644 --- a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts @@ -120,7 +120,7 @@ export class CreateSignatureShareCommand extends IronfishCommand { unsignedTransaction: UnsignedTransaction, frostSigningPackage: string, ): Promise { - const ledger = new LedgerMultiSigner(this.logger) + const ledger = new LedgerMultiSigner() try { await ledger.connect() } catch (e) { diff --git a/ironfish-cli/src/commands/wallet/transactions/sign.ts b/ironfish-cli/src/commands/wallet/transactions/sign.ts index 0445dc20e7..ca5c7ebc40 100644 --- a/ironfish-cli/src/commands/wallet/transactions/sign.ts +++ b/ironfish-cli/src/commands/wallet/transactions/sign.ts @@ -109,7 +109,7 @@ export class TransactionsSignCommand extends IronfishCommand { } private async signWithLedger(client: RpcClient, unsignedTransaction: string) { - const ledger = new LedgerSingleSigner(this.logger) + const ledger = new LedgerSingleSigner() try { await ledger.connect() } catch (e) { diff --git a/ironfish-cli/src/ledger/ledger.ts b/ironfish-cli/src/ledger/ledger.ts index d3776e64a7..620a312373 100644 --- a/ironfish-cli/src/ledger/ledger.ts +++ b/ironfish-cli/src/ledger/ledger.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Assert, createRootLogger, Logger } from '@ironfish/sdk' +import { Assert } from '@ironfish/sdk' import { StatusCodes, TransportStatusError } from '@ledgerhq/errors' import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' import IronfishApp, { @@ -19,14 +19,12 @@ export const IronfishLedgerStatusCodes = { export class Ledger { app: IronfishApp | undefined - logger: Logger PATH = "m/44'/1338'/0" isMultisig: boolean isConnecting: boolean = false - constructor(isMultisig: boolean, logger?: Logger) { + constructor(isMultisig: boolean) { this.app = undefined - this.logger = logger ? logger : createRootLogger() this.isMultisig = isMultisig } @@ -132,10 +130,6 @@ export class Ledger { this.app = undefined }) - if (transport.deviceModel) { - this.logger.debug(`${transport.deviceModel.productName} found.`) - } - const app = new IronfishApp(transport, this.isMultisig) this.app = app diff --git a/ironfish-cli/src/ledger/ledgerMultiSigner.ts b/ironfish-cli/src/ledger/ledgerMultiSigner.ts index 87489024e7..074628332f 100644 --- a/ironfish-cli/src/ledger/ledgerMultiSigner.ts +++ b/ironfish-cli/src/ledger/ledgerMultiSigner.ts @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Logger } from '@ironfish/sdk' import { IronfishKeys, KeyResponse, @@ -11,13 +10,11 @@ import { import { isResponseAddress, isResponseProofGenKey, isResponseViewKey, Ledger } from './ledger' export class LedgerMultiSigner extends Ledger { - constructor(logger?: Logger) { - super(true, logger) + constructor() { + super(true) } dkgGetIdentity = async (index: number): Promise => { - this.logger.debug('Retrieving identity from ledger device.') - const response = await this.tryInstruction((app) => app.dkgGetIdentity(index, false)) return response.identity @@ -28,8 +25,6 @@ export class LedgerMultiSigner extends Ledger { identities: string[], minSigners: number, ): Promise => { - this.logger.log('Please approve the request on your ledger device.') - return this.tryInstruction((app) => app.dkgRound1(index, identities, minSigners)) } @@ -38,8 +33,6 @@ export class LedgerMultiSigner extends Ledger { round1PublicPackages: string[], round1SecretPackage: string, ): Promise => { - this.logger.log('Please approve the request on your ledger device.') - return this.tryInstruction((app) => app.dkgRound2(index, round1PublicPackages, round1SecretPackage), ) @@ -53,8 +46,6 @@ export class LedgerMultiSigner extends Ledger { round2SecretPackage: string, gskBytes: string[], ): Promise => { - this.logger.log('Please approve the request on your ledger device.') - return this.tryInstruction((app) => app.dkgRound3Min( index, @@ -114,10 +105,6 @@ export class LedgerMultiSigner extends Ledger { } reviewTransaction = async (transaction: string): Promise => { - this.logger.info( - 'Please review and approve the outputs of this transaction on your ledger device.', - ) - const { hash } = await this.tryInstruction((app) => app.reviewTransaction(transaction)) return hash @@ -144,16 +131,12 @@ export class LedgerMultiSigner extends Ledger { } dkgBackupKeys = async (): Promise => { - this.logger.log('Please approve the request on your ledger device.') - const { encryptedKeys } = await this.tryInstruction((app) => app.dkgBackupKeys()) return encryptedKeys } dkgRestoreKeys = async (encryptedKeys: string): Promise => { - this.logger.log('Please approve the request on your ledger device.') - await this.tryInstruction((app) => app.dkgRestoreKeys(encryptedKeys)) } } diff --git a/ironfish-cli/src/ledger/ledgerSingleSigner.ts b/ironfish-cli/src/ledger/ledgerSingleSigner.ts index 36c641532e..8dc313a3a7 100644 --- a/ironfish-cli/src/ledger/ledgerSingleSigner.ts +++ b/ironfish-cli/src/ledger/ledgerSingleSigner.ts @@ -1,13 +1,13 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { ACCOUNT_SCHEMA_VERSION, AccountImport, Logger } from '@ironfish/sdk' +import { ACCOUNT_SCHEMA_VERSION, AccountImport } from '@ironfish/sdk' import { IronfishKeys, KeyResponse, ResponseSign } from '@zondax/ledger-ironfish' import { isResponseAddress, isResponseProofGenKey, isResponseViewKey, Ledger } from './ledger' export class LedgerSingleSigner extends Ledger { - constructor(logger?: Logger) { - super(false, logger) + constructor() { + super(false) } getPublicAddress = async () => { @@ -25,8 +25,6 @@ export class LedgerSingleSigner extends Ledger { importAccount = async () => { const publicAddress = await this.getPublicAddress() - this.logger.log('Please confirm the request on your ledger device.') - const responseViewKey: KeyResponse = await this.tryInstruction((app) => app.retrieveKeys(this.PATH, IronfishKeys.ViewKey, true), ) diff --git a/ironfish-cli/src/ui/ledger.ts b/ironfish-cli/src/ui/ledger.ts index 07d670a7ea..ae15f90a4c 100644 --- a/ironfish-cli/src/ui/ledger.ts +++ b/ironfish-cli/src/ui/ledger.ts @@ -93,7 +93,7 @@ export async function ledger({ } } else if (e instanceof LedgerActionRejected) { ux.action.status = 'User Rejected Ledger Request!' - ledger.logger.warn('User Rejected Ledger Request!') + ux.stdout('User Rejected Ledger Request!') } else if (e instanceof LedgerConnectError) { ux.action.status = 'Connect and unlock your Ledger' } else if (e instanceof LedgerAppNotOpen) { @@ -134,7 +134,7 @@ export async function sendTransactionWithLedger( confirm: boolean, logger?: Logger, ): Promise { - const ledgerApp = new LedgerSingleSigner(logger) + const ledgerApp = new LedgerSingleSigner() const publicKey = (await client.wallet.getAccountPublicKey({ account: from })).content .publicKey From 185ce24cd6a5574a4a264ae7a593b998bb2ab8cc Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 7 Oct 2024 17:33:57 -0700 Subject: [PATCH 48/73] Convert ui.retry to new confirmListPrompt (#5504) --- ironfish-cli/src/ui/ledger.ts | 24 +++++++++--------------- ironfish-cli/src/ui/prompt.ts | 23 +++++++++++++++++++++++ ironfish-cli/src/ui/retry.ts | 8 +++++--- 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/ironfish-cli/src/ui/ledger.ts b/ironfish-cli/src/ui/ledger.ts index ae15f90a4c..a245d463b1 100644 --- a/ironfish-cli/src/ui/ledger.ts +++ b/ironfish-cli/src/ui/ledger.ts @@ -12,7 +12,6 @@ import { Transaction, } from '@ironfish/sdk' import { Errors, ux } from '@oclif/core' -import inquirer from 'inquirer' import { Ledger, LedgerActionRejected, @@ -73,20 +72,15 @@ export async function ledger({ // cannot send any commands to the Ledger in the app's CLA. ux.action.stop('Ledger App Locked') - await inquirer.prompt<{ retry: boolean }>([ - { - name: 'retry', - message: `Ledger App Locked. Unlock and press enter to retry:`, - type: 'list', - choices: [ - { - name: `Retry`, - value: true, - default: true, - }, - ], - }, - ]) + const confirmed = await ui.confirmList( + 'Ledger App Locked. Unlock and press enter to retry:', + 'Retry', + ) + + if (!confirmed) { + ux.stdout('Operation aborted.') + ux.exit(0) + } if (!wasRunning) { ux.action.start(message) diff --git a/ironfish-cli/src/ui/prompt.ts b/ironfish-cli/src/ui/prompt.ts index 5685bfdd05..78c43e5e9a 100644 --- a/ironfish-cli/src/ui/prompt.ts +++ b/ironfish-cli/src/ui/prompt.ts @@ -138,6 +138,29 @@ export async function confirmPrompt(message: string): Promise { return result.prompt } +export async function confirmList(message: string, action = 'Confirm'): Promise { + const result = await inquirer.prompt<{ confirm: boolean }>([ + { + name: 'confirm', + message, + type: 'list', + choices: [ + { + name: action, + value: true, + default: true, + }, + { + name: 'Cancel', + value: false, + }, + ], + }, + ]) + + return result.confirm +} + export async function confirmOrQuit(message?: string, confirm?: boolean): Promise { if (confirm) { return diff --git a/ironfish-cli/src/ui/retry.ts b/ironfish-cli/src/ui/retry.ts index c016469515..addc2038eb 100644 --- a/ironfish-cli/src/ui/retry.ts +++ b/ironfish-cli/src/ui/retry.ts @@ -2,7 +2,8 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { ErrorUtils, Logger } from '@ironfish/sdk' -import { confirmPrompt } from './prompt' +import { ux } from '@oclif/core' +import { confirmList } from './prompt' export async function retryStep( stepFunction: () => Promise, @@ -20,9 +21,10 @@ export async function retryStep( logger.log(`An Error Occurred: ${ErrorUtils.renderError(error)}`) if (askToRetry) { - const continueResponse = await confirmPrompt('Do you want to retry this step?') + const continueResponse = await confirmList('Do you want to retry this step?', 'Retry') if (!continueResponse) { - throw new Error('User chose to not continue') + ux.stdout('User chose to not continue.') + ux.exit(0) } } } From ad44b93d6df211be429950d448c1b05a8d07a5b0 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Mon, 7 Oct 2024 19:37:06 -0700 Subject: [PATCH 49/73] Show account name public packages (#5506) --- .../src/commands/wallet/multisig/dkg/create.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index e82d54f2aa..d5321d8b62 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -169,6 +169,7 @@ export class DkgCreateCommand extends IronfishCommand { return this.performRound2( client, multisigClient, + accountName, participantName, round1, totalParticipants, @@ -466,6 +467,7 @@ export class DkgCreateCommand extends IronfishCommand { async performRound2( client: RpcClient, multisigClient: MultisigClient | null, + accountName: string, participantName: string, round1Result: { secretPackage: string; publicPackage: string }, totalParticipants: number, @@ -477,10 +479,10 @@ export class DkgCreateCommand extends IronfishCommand { let round1PublicPackages: string[] = [round1Result.publicPackage] if (!multisigClient) { this.log('\n============================================') - this.debug('\nRound 1 Encrypted Secret Package:') + this.debug(`\nRound 1 Encrypted Secret Package for ${accountName}:`) this.debug(round1Result.secretPackage) - this.log('\nRound 1 Public Package:') + this.log(`\nRound 1 Public Package for ${accountName}:`) this.log(round1Result.publicPackage) this.log('\n============================================') @@ -683,10 +685,10 @@ export class DkgCreateCommand extends IronfishCommand { let round2PublicPackages: string[] = [round2Result.publicPackage] if (!multisigClient) { this.log('\n============================================') - this.debug('\nRound 2 Encrypted Secret Package:') + this.debug(`\nRound 2 Encrypted Secret Package for ${accountName}:`) this.debug(round2Result.secretPackage) - this.log('\nRound 2 Public Package:') + this.log(`\nRound 2 Public Package for ${accountName}:`) this.log(round2Result.publicPackage) this.log('\n============================================') From b6fd99e6e342fe9d7168bc45d28ef128b0585be3 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Mon, 7 Oct 2024 20:33:22 -0700 Subject: [PATCH 50/73] Backup ledger keys in a separate step (#5505) This allows round3 and backup to fail independently and we can retry them separately. --- .../commands/wallet/multisig/dkg/create.ts | 76 +++++++++++-------- 1 file changed, 45 insertions(+), 31 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index d5321d8b62..30dc056196 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -198,10 +198,55 @@ export class DkgCreateCommand extends IronfishCommand { true, ) + if (ledger) { + await ui.retryStep( + async () => { + Assert.isNotUndefined(ledger) + return this.createBackup(ledger, accountName) + }, + this.logger, + true, + ) + } + this.log('Multisig account created successfully using DKG!') multisigClient?.stop() } + private async createBackup(ledger: LedgerMultiSigner, accountName: string) { + this.log() + this.log('Creating an encrypted backup of multisig keys from your Ledger device...') + this.log() + + const encryptedKeys = await ui.ledger({ + ledger, + message: 'Backup DKG Keys', + approval: true, + action: () => ledger.dkgBackupKeys(), + }) + + this.log() + this.log('Encrypted Ledger Multisig Backup:') + this.log(encryptedKeys.toString('hex')) + this.log() + this.log('Please save the encrypted keys shown above.') + this.log( + 'Use `ironfish wallet:multisig:ledger:restore` if you need to restore the keys to your Ledger.', + ) + + const dataDir = this.sdk.fileSystem.resolve(this.sdk.dataDir) + const backupKeysPath = path.join(dataDir, `ironfish-ledger-${accountName}.txt`) + + if (fs.existsSync(backupKeysPath)) { + await ui.confirmOrQuit( + `Error when backing up your keys: \nThe file ${backupKeysPath} already exists. \nOverwrite?`, + ) + } + + await fs.promises.writeFile(backupKeysPath, encryptedKeys.toString('hex')) + this.log(`A copy of your encrypted keys have been saved at ${backupKeysPath}`) + } + private async getOrCreateIdentity( client: RpcClient, ledger: LedgerMultiSigner | undefined, @@ -638,37 +683,6 @@ export class DkgCreateCommand extends IronfishCommand { this.log( `Account ${response.content.name} imported with public address: ${dkgKeys.publicAddress}`, ) - this.log() - this.log('Creating an encrypted backup of multisig keys from your Ledger device...') - this.log() - - const encryptedKeys = await ui.ledger({ - ledger, - message: 'Backup DKG Keys', - approval: true, - action: () => ledger.dkgBackupKeys(), - }) - - this.log() - this.log('Encrypted Ledger Multisig Backup:') - this.log(encryptedKeys.toString('hex')) - this.log() - this.log('Please save the encrypted keys shown above.') - this.log( - 'Use `ironfish wallet:multisig:ledger:restore` if you need to restore the keys to your Ledger.', - ) - - const dataDir = this.sdk.fileSystem.resolve(this.sdk.dataDir) - const backupKeysPath = path.join(dataDir, `ironfish-ledger-${accountName}.txt`) - - if (fs.existsSync(backupKeysPath)) { - await ui.confirmOrQuit( - `Error when backing up your keys: \nThe file ${backupKeysPath} already exists. \nOverwrite?`, - ) - } - - await fs.promises.writeFile(backupKeysPath, encryptedKeys.toString('hex')) - this.log(`A copy of your encrypted keys have been saved at ${backupKeysPath}`) } async performRound3( From fc6db867f8866d43cfbedf2facf5501eaa8a22ce Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Mon, 7 Oct 2024 20:45:29 -0700 Subject: [PATCH 51/73] Unhandled exception will stop UI action (#5507) --- ironfish-cli/src/ui/ledger.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ironfish-cli/src/ui/ledger.ts b/ironfish-cli/src/ui/ledger.ts index a245d463b1..e6c50ae45d 100644 --- a/ironfish-cli/src/ui/ledger.ts +++ b/ironfish-cli/src/ui/ledger.ts @@ -39,7 +39,6 @@ export async function ledger({ approval?: boolean }): Promise { const wasRunning = ux.action.running - let statusAdded = false if (!wasRunning) { ux.action.start(message) @@ -106,14 +105,13 @@ export async function ledger({ throw e } - statusAdded = true await PromiseUtils.sleep(1000) continue } } } finally { // Don't interrupt an existing status outside of ledgerAction() - if (!wasRunning && statusAdded) { + if (!wasRunning) { clearTimeout(clearStatusTimer) ux.action.stop() } From df3f0969ce6e8d4e43f901ac8e2f8d3cdabc8cdb Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Mon, 7 Oct 2024 20:56:20 -0700 Subject: [PATCH 52/73] unside chainport:send (#5461) --- ironfish-cli/src/commands/wallet/chainport/send.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ironfish-cli/src/commands/wallet/chainport/send.ts b/ironfish-cli/src/commands/wallet/chainport/send.ts index 1612feeb79..93595ba3e9 100644 --- a/ironfish-cli/src/commands/wallet/chainport/send.ts +++ b/ironfish-cli/src/commands/wallet/chainport/send.ts @@ -35,7 +35,6 @@ import { watchTransaction } from '../../../utils/transaction' export class BridgeCommand extends IronfishCommand { static description = `Use the Chainport bridge to bridge assets to EVM networks.` - static hidden = true static flags = { ...RemoteFlags, From 9ebb97918fa8a5d43c8b5344e3a56fd29c16e3d4 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 7 Oct 2024 21:02:43 -0700 Subject: [PATCH 53/73] Fix rejection type on C vs Rust SDK (#5508) --- ironfish-cli/src/ledger/ledger.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ironfish-cli/src/ledger/ledger.ts b/ironfish-cli/src/ledger/ledger.ts index 620a312373..2f3d5358f7 100644 --- a/ironfish-cli/src/ledger/ledger.ts +++ b/ironfish-cli/src/ledger/ledger.ts @@ -93,6 +93,13 @@ export class Ledger { throw new LedgerClaNotSupportedError() } else if (error.returnCode === IronfishLedgerStatusCodes.GP_AUTH_FAILED) { throw new LedgerGPAuthFailed() + } else if ( + [ + IronfishLedgerStatusCodes.COMMAND_NOT_ALLOWED, + IronfishLedgerStatusCodes.CONDITIONS_OF_USE_NOT_SATISFIED, + ].includes(error.returnCode) + ) { + throw new LedgerActionRejected() } else if ( [ IronfishLedgerStatusCodes.INS_NOT_SUPPORTED, From 74a9f95b6bdb00778b92bf449e28a166aadddbfb Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 7 Oct 2024 21:07:30 -0700 Subject: [PATCH 54/73] Set approve timeout greater than connect timeout (#5509) This fix ensures that the timeout threshold to check the user should approve the action is more than the connect timeout. --- ironfish-cli/src/ledger/ledger.ts | 3 ++- ironfish-cli/src/ui/ledger.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ironfish-cli/src/ledger/ledger.ts b/ironfish-cli/src/ledger/ledger.ts index 2f3d5358f7..7fa0184c48 100644 --- a/ironfish-cli/src/ledger/ledger.ts +++ b/ironfish-cli/src/ledger/ledger.ts @@ -22,6 +22,7 @@ export class Ledger { PATH = "m/44'/1338'/0" isMultisig: boolean isConnecting: boolean = false + connectTimeout = 2000 constructor(isMultisig: boolean) { this.app = undefined @@ -130,7 +131,7 @@ export class Ledger { let transport: Transport | undefined = undefined try { - transport = await TransportNodeHid.create(2000, 2000) + transport = await TransportNodeHid.create(this.connectTimeout, this.connectTimeout) transport.on('disconnect', async () => { await transport?.close() diff --git a/ironfish-cli/src/ui/ledger.ts b/ironfish-cli/src/ui/ledger.ts index e6c50ae45d..6975a92685 100644 --- a/ironfish-cli/src/ui/ledger.ts +++ b/ironfish-cli/src/ui/ledger.ts @@ -56,7 +56,7 @@ export async function ledger({ } else { ux.action.status = undefined } - }, 1500) + }, ledger.connectTimeout + 500) const result = await action() ux.action.stop() From 27db10eec6921fc4ff3f2ec3eb5a77de9f7c8c0b Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:44:27 -0400 Subject: [PATCH 55/73] feat(cli): Update transaction summary for chainport transactions (#5494) * Update bridge transaction summary * More updates to chainport transaction summary * Remove 30m warning log --------- Co-authored-by: Derek Guenther --- .../src/commands/wallet/transactions/info.ts | 22 +++-- ironfish-cli/src/utils/chainport/utils.ts | 93 ++++++++++--------- 2 files changed, 64 insertions(+), 51 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/transactions/info.ts b/ironfish-cli/src/commands/wallet/transactions/info.ts index 7b6be0d108..2a49f48418 100644 --- a/ironfish-cli/src/commands/wallet/transactions/info.ts +++ b/ironfish-cli/src/commands/wallet/transactions/info.ts @@ -14,6 +14,7 @@ import { IronfishCommand } from '../../../command' import { RemoteFlags } from '../../../flags' import * as ui from '../../../ui' import { + ChainportNetwork, displayChainportTransactionSummary, extractChainportDataFromTransaction, fetchChainportNetworks, @@ -99,12 +100,21 @@ export class TransactionInfoCommand extends IronfishCommand { if (chainportTxnDetails) { this.log(`\n---Chainport Bridge Transaction Summary---\n`) - ux.action.start('Fetching network details') - const chainportNetworks = await fetchChainportNetworks(networkId) - const network = chainportNetworks.find( - (n) => n.chainport_network_id === chainportTxnDetails.chainportNetworkId, - ) - ux.action.stop() + let network: ChainportNetwork | undefined + try { + ux.action.start('Fetching network details') + const chainportNetworks = await fetchChainportNetworks(networkId) + network = chainportNetworks.find( + (n) => n.chainport_network_id === chainportTxnDetails.chainportNetworkId, + ) + ux.action.stop() + } catch (e: unknown) { + ux.action.stop('error') + + if (e instanceof Error) { + this.logger.debug(e.message) + } + } await displayChainportTransactionSummary( networkId, diff --git a/ironfish-cli/src/utils/chainport/utils.ts b/ironfish-cli/src/utils/chainport/utils.ts index c868bd7ce7..9961ff3422 100644 --- a/ironfish-cli/src/utils/chainport/utils.ts +++ b/ironfish-cli/src/utils/chainport/utils.ts @@ -2,18 +2,12 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { - defaultNetworkName, - Logger, - RpcWalletTransaction, - TransactionStatus, - TransactionType, -} from '@ironfish/sdk' +import { Logger, RpcWalletTransaction, TransactionStatus, TransactionType } from '@ironfish/sdk' import { ux } from '@oclif/core' import { getConfig, isNetworkSupportedByChainport } from './config' import { ChainportMemoMetadata } from './metadata' import { fetchChainportTransactionStatus } from './requests' -import { ChainportNetwork } from './types' +import { ChainportNetwork, ChainportTransactionStatus } from './types' export type ChainportTransactionData = | { @@ -105,76 +99,85 @@ export const displayChainportTransactionSummary = async ( return } - if (!network) { - logger.log( - `This transaction is a ${ - data.type === TransactionType.SEND ? 'outgoing' : 'incoming' - } chainport bridge transaction. Error fetching network details.`, - ) - return - } - // Chainport does not give us a way to determine the source transaction hash of an incoming bridge transaction // So we can only display the source network and address if (data.type === TransactionType.RECEIVE) { logger.log(` Direction: Incoming -Source Network: ${network.label} +Source Network: ${network?.label ?? 'Error fetching network details'} Address: ${data.address} - Explorer Account: ${network.explorer_url + 'address/' + data.address} -Target (Ironfish) Network: ${defaultNetworkName(networkId)}`) + Explorer Account: ${ + network + ? new URL('address/' + data.address, network.explorer_url).toString() + : 'Error fetching network details' + }`) return } const basicInfo = ` Direction: Outgoing -============================================== -Source Network: ${defaultNetworkName(networkId)} - Transaction Status: ${transaction.status} - Transaction Hash: ${transaction.hash} -============================================== -Target Network: ${network.label} +Target Network: ${network?.label ?? 'Error fetching network details'} Address: ${data.address} - Explorer Account: ${network.explorer_url + 'address/' + data.address}` + Explorer Account: ${ + network + ? new URL('address/' + data.address, network.explorer_url).toString() + : 'Error fetching network details' + }` - // We'll wait to show the transaction status if the transaction is still pending on Ironfish + // We'll wait to show the transaction status if the transaction is still pending on Iron Fish if (transaction.status !== TransactionStatus.CONFIRMED) { logger.log(basicInfo) + logger.log(` Transaction Status: ${transaction.status} (Iron Fish)`) return } ux.action.start('Fetching transaction information on target network') - const transactionStatus = await fetchChainportTransactionStatus(networkId, transaction.hash) - ux.action.stop() + let transactionStatus: ChainportTransactionStatus | undefined + try { + transactionStatus = await fetchChainportTransactionStatus(networkId, transaction.hash) + ux.action.stop() + } catch (e: unknown) { + ux.action.stop('error') + + if (e instanceof Error) { + logger.debug(e.message) + } + } logger.log(basicInfo) - if (Object.keys(transactionStatus).length === 0) { - logger.log(` -Transaction status not found on target network. -Note: Bridge transactions may take up to 30 minutes to surface on the target network. -If this issue persists, please contact chainport support: https://helpdesk.chainport.io/`) + if (!transactionStatus) { + logger.log(` Transaction Status: Error fetching transaction details`) + return + } + + // States taken from https://docs.chainport.io/for-developers/api-reference/port + if (Object.keys(transactionStatus).length === 0 || !transactionStatus.base_tx_status) { + logger.log(` Transaction Status: Pending confirmation (Iron Fish)`) return } if ( - !transactionStatus.base_tx_hash || - !transactionStatus.base_tx_status || + transactionStatus.base_tx_hash && + transactionStatus.base_tx_status === 1 && !transactionStatus.target_tx_hash ) { - logger.log(` Transaction Status: pending`) + logger.log(` Transaction Status: Pending creation (target network)`) return } - if (transactionStatus.target_tx_status === 1) { - logger.log(` Transaction Status: completed`) - } else { - logger.log(` Transaction Status: in progress`) - } - + logger.log( + ` Transaction Status: ${ + transactionStatus.target_tx_status === 1 + ? 'Completed' + : 'Pending confirmation (target network)' + }`, + ) logger.log(` Transaction Hash: ${transactionStatus.target_tx_hash} Explorer Transaction: ${ - network.explorer_url + 'tx/' + transactionStatus.target_tx_hash + network + ? new URL('address/' + data.address, network.explorer_url).toString() + : 'Error fetching network details' }`) } From e7221195cad39ee6ae7acd15150feba0b8c5efa4 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 8 Oct 2024 13:54:38 -0700 Subject: [PATCH 56/73] Avoid connect handshake and handle all error cases (#5512) This adds new documentation about what and which errors are thrown. It also removes appInfo() and getVersion() during tryInstruction because it duplicates the error cdoes you get from just running any app commands. This also transforms ledger app errors from TransportStatusError into ResponseError so we can handle all the error codes in a single way. --- ironfish-cli/src/ledger/README.md | 33 +++++++++++++++++ ironfish-cli/src/ledger/ledger.ts | 61 +++++++------------------------ 2 files changed, 47 insertions(+), 47 deletions(-) create mode 100644 ironfish-cli/src/ledger/README.md diff --git a/ironfish-cli/src/ledger/README.md b/ironfish-cli/src/ledger/README.md new file mode 100644 index 0000000000..34c55dc969 --- /dev/null +++ b/ironfish-cli/src/ledger/README.md @@ -0,0 +1,33 @@ +# Ledger + +#### IronfishApp.appInfo() (OS CLA) + C APP + If Dashboard Open: + If Locked: throw 0x5515 DeviceLocked + If Unlocked: throw 0x6e01 (APP NOT OPEN) + If App Open: + If Locked: throw 0x5515 Device Locked + If Unlocked: returns successfully + RUST APP (OS RPC) + If Dashboard Open: + If Locked: throw 0x5515 DeviceLocked + If Unlocked: throw 0x6e01 (APP NOT OPEN) + If App Open: + If Locked: throw INS_NOT_SUPPORTED + If Unlocked: returns successfully + +##### IronfishApp.getVersion (APP CLA) + C APP + If Dashboard Open: + If Locked: throw 0x5515 DeviceLocked + If Unlocked: throw 0x6e01 (APP NOT OPEN) + If App Open: + If Locked: throw 0x5515 Device Locked + If Unlocked: returns successfully + RUST APP + If Dashboard Open: + If Locked: throw 0x5515 DeviceLocked + If Unlocked: throw 0x6e01 (APP NOT OPEN) + If App Open: + If Locked: throw INS_NOT_SUPPORTED + If Unlocked: returns successfully() diff --git a/ironfish-cli/src/ledger/ledger.ts b/ironfish-cli/src/ledger/ledger.ts index 7fa0184c48..d4538c7003 100644 --- a/ironfish-cli/src/ledger/ledger.ts +++ b/ironfish-cli/src/ledger/ledger.ts @@ -15,6 +15,8 @@ import { ResponseError, Transport } from '@zondax/ledger-js' export const IronfishLedgerStatusCodes = { ...StatusCodes, COMMAND_NOT_ALLOWED: 0x6986, + APP_NOT_OPEN: 0x6e01, + UNKNOWN_TRANSPORT_ERROR: 0xffff, } export class Ledger { @@ -35,61 +37,25 @@ export class Ledger { Assert.isNotUndefined(this.app, 'Unable to establish connection with Ledger device') - const app = this.app - - // App info is a request to the dashboard CLA. The purpose of this it to - // produce a Locked Device error and works if an app is open or closed. - try { - await app.appInfo() - } catch (error) { - if ( - error instanceof ResponseError && - error.message.includes('Attempt to read beyond buffer length') && - error.returnCode === IronfishLedgerStatusCodes.TECHNICAL_PROBLEM - ) { - // Catch this error and swollow it until the SDK fix merges to fix - // this - } - } - - // This is an app specific request. This is useful because this throws - // INS_NOT_SUPPORTED in the case that the app is locked which is useful to - // know versus the device is locked. - try { - await app.getVersion() - } catch (error) { - if ( - error instanceof ResponseError && - error.returnCode === IronfishLedgerStatusCodes.INS_NOT_SUPPORTED - ) { - throw new LedgerAppLocked() - } + return await instruction(this.app) + } catch (e: unknown) { + let error = e - throw error - } - - return await instruction(app) - } catch (error: unknown) { - if (LedgerPortIsBusyError.IsError(error)) { + if (LedgerPortIsBusyError.IsError(e)) { throw new LedgerPortIsBusyError() - } else if (LedgerConnectError.IsError(error)) { + } else if (LedgerConnectError.IsError(e)) { throw new LedgerConnectError() } if (error instanceof TransportStatusError) { - if ( - error.statusCode === IronfishLedgerStatusCodes.COMMAND_NOT_ALLOWED || - error.statusCode === IronfishLedgerStatusCodes.CONDITIONS_OF_USE_NOT_SATISFIED - ) { - throw new LedgerActionRejected() - } else { - throw new LedgerConnectError() - } + error = new ResponseError(error.statusCode, error.statusText) } if (error instanceof ResponseError) { if (error.returnCode === IronfishLedgerStatusCodes.LOCKED_DEVICE) { throw new LedgerDeviceLockedError() + } else if (error.returnCode === IronfishLedgerStatusCodes.INS_NOT_SUPPORTED) { + throw new LedgerAppLocked() } else if (error.returnCode === IronfishLedgerStatusCodes.CLA_NOT_SUPPORTED) { throw new LedgerClaNotSupportedError() } else if (error.returnCode === IronfishLedgerStatusCodes.GP_AUTH_FAILED) { @@ -103,15 +69,16 @@ export class Ledger { throw new LedgerActionRejected() } else if ( [ - IronfishLedgerStatusCodes.INS_NOT_SUPPORTED, IronfishLedgerStatusCodes.TECHNICAL_PROBLEM, - 0xffff, // Unknown transport error - 0x6e01, // App not open + IronfishLedgerStatusCodes.UNKNOWN_TRANSPORT_ERROR, + IronfishLedgerStatusCodes.APP_NOT_OPEN, ].includes(error.returnCode) ) { throw new LedgerAppNotOpen( `Unable to connect to Ironfish app on Ledger. Please check that the device is unlocked and the app is open.`, ) + } else if (e instanceof TransportStatusError) { + throw new LedgerConnectError() } throw new LedgerError(error.message) From 5cb1b635eb437288300545e13be27e3f3f42edc8 Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Tue, 8 Oct 2024 18:13:17 -0400 Subject: [PATCH 57/73] Remove a stray apostrophe from NotEnoughFunds message (#5514) --- ironfish/src/rpc/routes/wallet/createTransaction.ts | 2 +- ironfish/src/rpc/routes/wallet/sendTransaction.ts | 2 +- ironfish/src/wallet/errors.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ironfish/src/rpc/routes/wallet/createTransaction.ts b/ironfish/src/rpc/routes/wallet/createTransaction.ts index ca99db54ab..b878659bb8 100644 --- a/ironfish/src/rpc/routes/wallet/createTransaction.ts +++ b/ironfish/src/rpc/routes/wallet/createTransaction.ts @@ -236,7 +236,7 @@ routes.register( assetData, ) const renderedAmount = CurrencyUtils.render(e.amount, false, e.assetId, assetData) - const message = `Insufficient funds: Needed ${renderedAmountNeeded} but have ${renderedAmount} available to spend. Please fund your account and/or wait for any pending transactions to be confirmed.'` + const message = `Insufficient funds: Needed ${renderedAmountNeeded} but have ${renderedAmount} available to spend. Please fund your account and/or wait for any pending transactions to be confirmed.` throw new RpcValidationError(message, 400, RPC_ERROR_CODES.INSUFFICIENT_BALANCE) } throw e diff --git a/ironfish/src/wallet/errors.ts b/ironfish/src/wallet/errors.ts index e0d9c0c16e..6cb3a5a9a2 100644 --- a/ironfish/src/wallet/errors.ts +++ b/ironfish/src/wallet/errors.ts @@ -19,7 +19,7 @@ export class NotEnoughFundsError extends Error { const renderedAmountNeeded = CurrencyUtils.render(amountNeeded, true, this.assetId) const renderedAmount = CurrencyUtils.render(amount) - this.message = `Insufficient funds: Needed ${renderedAmountNeeded} but have ${renderedAmount} available to spend. Please fund your account and/or wait for any pending transactions to be confirmed.'` + this.message = `Insufficient funds: Needed ${renderedAmountNeeded} but have ${renderedAmount} available to spend. Please fund your account and/or wait for any pending transactions to be confirmed.` } } From 5e2b6ba449d06a4fb98bb02f0de31f940276dc8a Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:14:06 -0700 Subject: [PATCH 58/73] reviews tx on InvalidTxHash error (#5513) * always reviews tx in same connection as commit or sign clear signing in the Ironfish DKG Ledger App requires that the client review the transaction with 'reviewTransaction' before generating signing commitments with 'dkgGetCommitments' or creating a signature share with 'dkgSign' an approved 'reviewTransaction' instruction stores the transaction hash of the approved transaction in the device memory. however, storage of that hash does not persist between transport connections. so, if a connection is lost due to an error, or if a new connection is created for each instruction, then the hash cannot be accessed to allow generation of commitments or signature shares modifies 'dkgGetCommitments' and 'dkgSign' in 'LedgerMultiSigner' to take an unsigned transaction and always call the 'reviewTransaction' instruction in the same connection as 'dkgGetCommitments' and 'dkgSign' * reviews tx on InvalidTxHash error updates dkgGetCommitments and dkgSign to catch Ledger error caused by unreviewed transaction (InvalidTxHash), call reviewTransaction, and finally recursively call themselves adds error type for LedgerInvalidTxHash --- .../wallet/multisig/commitment/create.ts | 8 +-- .../src/commands/wallet/multisig/sign.ts | 20 +++---- .../wallet/multisig/signature/create.ts | 10 +--- ironfish-cli/src/ledger/ledger.ts | 4 ++ ironfish-cli/src/ledger/ledgerMultiSigner.ts | 54 ++++++++++++++----- 5 files changed, 54 insertions(+), 42 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts index fd2a53181c..f2e9103da2 100644 --- a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts @@ -138,16 +138,12 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { const identityResponse = await client.wallet.multisig.getIdentity({ name: participantName }) const identity = identityResponse.content.identity - const transactionHash = await ledger.reviewTransaction( - unsignedTransaction.serialize().toString('hex'), - ) - - const rawCommitments = await ledger.dkgGetCommitments(transactionHash.toString('hex')) + const rawCommitments = await ledger.dkgGetCommitments(unsignedTransaction) const signingCommitment = multisig.SigningCommitment.fromRaw( identity, rawCommitments, - transactionHash, + unsignedTransaction.hash(), signers, ) diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts index 59957487f4..65c42d35ad 100644 --- a/ironfish-cli/src/commands/wallet/multisig/sign.ts +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -381,11 +381,11 @@ export class SignMultisigTransactionCommand extends IronfishCommand { const frostSignatureShare = await ui.ledger({ ledger, message: 'Sign Transaction', + approval: true, action: () => ledger.dkgSign( - unsignedTransaction.publicKeyRandomness(), + unsignedTransaction, signingPackage.frostSigningPackage().toString('hex'), - unsignedTransaction.hash().toString('hex'), ), }) @@ -511,17 +511,10 @@ export class SignMultisigTransactionCommand extends IronfishCommand { let commitment if (ledger) { - await ui.ledger({ - ledger, - message: 'Review Transaction', - action: () => ledger.reviewTransaction(unsignedTransactionHex), - approval: true, - }) - commitment = await this.createSigningCommitmentWithLedger( ledger, participant, - unsignedTransaction.hash(), + unsignedTransaction, identities, ) } else { @@ -543,19 +536,20 @@ export class SignMultisigTransactionCommand extends IronfishCommand { async createSigningCommitmentWithLedger( ledger: LedgerMultiSigner, participant: MultisigParticipant, - transactionHash: Buffer, + unsignedTransaction: UnsignedTransaction, signers: string[], ): Promise { const rawCommitments = await ui.ledger({ ledger, message: 'Get Commitments', - action: () => ledger.dkgGetCommitments(transactionHash.toString('hex')), + approval: true, + action: () => ledger.dkgGetCommitments(unsignedTransaction), }) const sigingCommitment = multisig.SigningCommitment.fromRaw( participant.identity, rawCommitments, - transactionHash, + unsignedTransaction.hash(), signers, ) diff --git a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts index 2d487a204c..7bc41efa17 100644 --- a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts @@ -134,15 +134,7 @@ export class CreateSignatureShareCommand extends IronfishCommand { const identityResponse = await client.wallet.multisig.getIdentity({ name: participantName }) const identity = identityResponse.content.identity - const transactionHash = await ledger.reviewTransaction( - unsignedTransaction.serialize().toString('hex'), - ) - - const frostSignatureShare = await ledger.dkgSign( - unsignedTransaction.publicKeyRandomness(), - frostSigningPackage, - transactionHash.toString('hex'), - ) + const frostSignatureShare = await ledger.dkgSign(unsignedTransaction, frostSigningPackage) const signatureShare = multisig.SignatureShare.fromFrost( frostSignatureShare, diff --git a/ironfish-cli/src/ledger/ledger.ts b/ironfish-cli/src/ledger/ledger.ts index d4538c7003..133ffbbdb4 100644 --- a/ironfish-cli/src/ledger/ledger.ts +++ b/ironfish-cli/src/ledger/ledger.ts @@ -17,6 +17,7 @@ export const IronfishLedgerStatusCodes = { COMMAND_NOT_ALLOWED: 0x6986, APP_NOT_OPEN: 0x6e01, UNKNOWN_TRANSPORT_ERROR: 0xffff, + INVALID_TX_HASH: 0xb025, } export class Ledger { @@ -77,6 +78,8 @@ export class Ledger { throw new LedgerAppNotOpen( `Unable to connect to Ironfish app on Ledger. Please check that the device is unlocked and the app is open.`, ) + } else if (error.returnCode === IronfishLedgerStatusCodes.INVALID_TX_HASH) { + throw new LedgerInvalidTxHash() } else if (e instanceof TransportStatusError) { throw new LedgerConnectError() } @@ -161,3 +164,4 @@ export class LedgerGPAuthFailed extends LedgerError {} export class LedgerClaNotSupportedError extends LedgerError {} export class LedgerAppNotOpen extends LedgerError {} export class LedgerActionRejected extends LedgerError {} +export class LedgerInvalidTxHash extends LedgerError {} diff --git a/ironfish-cli/src/ledger/ledgerMultiSigner.ts b/ironfish-cli/src/ledger/ledgerMultiSigner.ts index 074628332f..17ee769fd2 100644 --- a/ironfish-cli/src/ledger/ledgerMultiSigner.ts +++ b/ironfish-cli/src/ledger/ledgerMultiSigner.ts @@ -1,13 +1,20 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { UnsignedTransaction } from '@ironfish/sdk' import { IronfishKeys, KeyResponse, ResponseDkgRound1, ResponseDkgRound2, } from '@zondax/ledger-ironfish' -import { isResponseAddress, isResponseProofGenKey, isResponseViewKey, Ledger } from './ledger' +import { + isResponseAddress, + isResponseProofGenKey, + isResponseViewKey, + Ledger, + LedgerInvalidTxHash, +} from './ledger' export class LedgerMultiSigner extends Ledger { constructor() { @@ -110,24 +117,43 @@ export class LedgerMultiSigner extends Ledger { return hash } - dkgGetCommitments = async (transactionHash: string): Promise => { - const { commitments } = await this.tryInstruction((app) => - app.dkgGetCommitments(transactionHash), - ) - - return commitments + dkgGetCommitments = async (transaction: UnsignedTransaction): Promise => { + try { + const { commitments } = await this.tryInstruction(async (app) => { + return app.dkgGetCommitments(transaction.hash().toString('hex')) + }) + return commitments + } catch (e) { + if (e instanceof LedgerInvalidTxHash) { + await this.reviewTransaction(transaction.serialize().toString('hex')) + return this.dkgGetCommitments(transaction) + } + + throw e + } } dkgSign = async ( - randomness: string, + transaction: UnsignedTransaction, frostSigningPackage: string, - transactionHash: string, ): Promise => { - const { signature } = await this.tryInstruction((app) => - app.dkgSign(randomness, frostSigningPackage, transactionHash), - ) - - return signature + try { + const { signature } = await this.tryInstruction(async (app) => { + return app.dkgSign( + transaction.publicKeyRandomness(), + frostSigningPackage, + transaction.hash().toString('hex'), + ) + }) + return signature + } catch (e) { + if (e instanceof LedgerInvalidTxHash) { + await this.reviewTransaction(transaction.serialize().toString('hex')) + return this.dkgSign(transaction, frostSigningPackage) + } + + throw e + } } dkgBackupKeys = async (): Promise => { From 9b3f5d638e03f4b5e0f5c97941905a84e827f490 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 8 Oct 2024 15:20:09 -0700 Subject: [PATCH 59/73] Handle DeviceDisconnected Error (#5515) This happens during operations occasionally when the transport fails. --- ironfish-cli/src/ledger/ledger.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ironfish-cli/src/ledger/ledger.ts b/ironfish-cli/src/ledger/ledger.ts index 133ffbbdb4..f1c16c3f44 100644 --- a/ironfish-cli/src/ledger/ledger.ts +++ b/ironfish-cli/src/ledger/ledger.ts @@ -2,7 +2,11 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { Assert } from '@ironfish/sdk' -import { StatusCodes, TransportStatusError } from '@ledgerhq/errors' +import { + DisconnectedDeviceDuringOperation, + StatusCodes, + TransportStatusError, +} from '@ledgerhq/errors' import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' import IronfishApp, { KeyResponse, @@ -46,6 +50,8 @@ export class Ledger { throw new LedgerPortIsBusyError() } else if (LedgerConnectError.IsError(e)) { throw new LedgerConnectError() + } else if (e instanceof DisconnectedDeviceDuringOperation) { + throw new LedgerConnectError() } if (error instanceof TransportStatusError) { From bf000b2be771ab1c8abb16280ca177922f9df46d Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 8 Oct 2024 15:33:16 -0700 Subject: [PATCH 60/73] Handle ledger app panics (#5516) --- ironfish-cli/src/ledger/ledger.ts | 4 ++++ ironfish-cli/src/ui/ledger.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/ironfish-cli/src/ledger/ledger.ts b/ironfish-cli/src/ledger/ledger.ts index f1c16c3f44..ecf4835484 100644 --- a/ironfish-cli/src/ledger/ledger.ts +++ b/ironfish-cli/src/ledger/ledger.ts @@ -22,6 +22,7 @@ export const IronfishLedgerStatusCodes = { APP_NOT_OPEN: 0x6e01, UNKNOWN_TRANSPORT_ERROR: 0xffff, INVALID_TX_HASH: 0xb025, + PANIC: 0xe000, } export class Ledger { @@ -65,6 +66,8 @@ export class Ledger { throw new LedgerAppLocked() } else if (error.returnCode === IronfishLedgerStatusCodes.CLA_NOT_SUPPORTED) { throw new LedgerClaNotSupportedError() + } else if (error.returnCode === IronfishLedgerStatusCodes.PANIC) { + throw new LedgerPanicError() } else if (error.returnCode === IronfishLedgerStatusCodes.GP_AUTH_FAILED) { throw new LedgerGPAuthFailed() } else if ( @@ -171,3 +174,4 @@ export class LedgerClaNotSupportedError extends LedgerError {} export class LedgerAppNotOpen extends LedgerError {} export class LedgerActionRejected extends LedgerError {} export class LedgerInvalidTxHash extends LedgerError {} +export class LedgerPanicError extends LedgerError {} diff --git a/ironfish-cli/src/ui/ledger.ts b/ironfish-cli/src/ui/ledger.ts index 6975a92685..52b35f117b 100644 --- a/ironfish-cli/src/ui/ledger.ts +++ b/ironfish-cli/src/ui/ledger.ts @@ -21,6 +21,7 @@ import { LedgerConnectError, LedgerDeviceLockedError, LedgerGPAuthFailed, + LedgerPanicError, LedgerPortIsBusyError, LedgerSingleSigner, } from '../ledger' @@ -94,6 +95,9 @@ export async function ledger({ ux.action.status = `Open Ledger App ${appName}` } else if (e instanceof LedgerDeviceLockedError) { ux.action.status = 'Unlock Ledger' + } else if (e instanceof LedgerPanicError) { + ux.action.status = 'Ledger App Crashed' + ux.stdout('Ledger App Crashed! ⚠️') } else if (e instanceof LedgerPortIsBusyError) { ux.action.status = 'Ledger is busy, retrying' } else if (e instanceof LedgerGPAuthFailed) { From 9f4abf67347cb013c3471b61d89ec976635bd03b Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Tue, 8 Oct 2024 15:36:32 -0700 Subject: [PATCH 61/73] use numberprompt for total participants in multisig sign (#5517) --- ironfish-cli/src/commands/wallet/multisig/sign.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts index 65c42d35ad..1d6f05b606 100644 --- a/ironfish-cli/src/commands/wallet/multisig/sign.ts +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -253,11 +253,11 @@ export class SignMultisigTransactionCommand extends IronfishCommand { Buffer.from(unsignedTransactionInput, 'hex'), ) - const input = await ui.inputPrompt( + const totalParticipants = await ui.inputNumberPrompt( + this.logger, 'Enter the number of participants in signing this transaction', - true, + { required: true, integer: true }, ) - const totalParticipants = parseInt(input) if (totalParticipants < 2) { this.error('Minimum number of participants must be at least 2') From 4781602b8c3cc4846d8152efe940835dd19224fd Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 8 Oct 2024 15:51:02 -0700 Subject: [PATCH 62/73] Upgrade ledger-ironfish-js to 1.0.0 (#5518) --- ironfish-cli/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index 6e11745999..944ca5e887 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -70,7 +70,7 @@ "@oclif/plugin-warn-if-update-available": "3.1.8", "@types/keccak": "3.0.4", "@types/tar": "6.1.1", - "@zondax/ledger-ironfish": "0.5.1", + "@zondax/ledger-ironfish": "1.0.0", "@zondax/ledger-js": "1.0.1", "axios": "1.7.2", "bech32": "2.0.0", diff --git a/yarn.lock b/yarn.lock index 8f6900b489..d993f18f0b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3902,10 +3902,10 @@ dependencies: argparse "^2.0.1" -"@zondax/ledger-ironfish@0.5.1": - version "0.5.1" - resolved "https://registry.yarnpkg.com/@zondax/ledger-ironfish/-/ledger-ironfish-0.5.1.tgz#c628a625d2f66280c74fc2859c70ac059451e8e4" - integrity sha512-yzoyejbz5kRFSD3D3u2pIkEOwmAWWKBXiVr7ssb5TRXdimLUTHFgST7CIMp1iqRrkw8bw6HcM7RKcGol5bd9xQ== +"@zondax/ledger-ironfish@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@zondax/ledger-ironfish/-/ledger-ironfish-1.0.0.tgz#f1831b44e75d74a372d7bcd4bbb4c5df76731211" + integrity sha512-eWbvTP2pwqiRylWuA8YybcX7V7Iw2zcvYIhQ7cYjyZCI6ruDVO7bEdi/d2Q8Wfkjq9vOadHeoVZYAaVk/ulBMw== dependencies: "@zondax/ledger-js" "^1.0.1" From 33887004017b5f6627855b52ea0c3d6882d14982 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 8 Oct 2024 16:11:38 -0700 Subject: [PATCH 63/73] Handle ledger DisconnectDevice error (#5519) --- ironfish-cli/src/ledger/ledger.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ironfish-cli/src/ledger/ledger.ts b/ironfish-cli/src/ledger/ledger.ts index ecf4835484..0649309dd3 100644 --- a/ironfish-cli/src/ledger/ledger.ts +++ b/ironfish-cli/src/ledger/ledger.ts @@ -3,6 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { Assert } from '@ironfish/sdk' import { + DisconnectedDevice, DisconnectedDeviceDuringOperation, StatusCodes, TransportStatusError, @@ -53,6 +54,8 @@ export class Ledger { throw new LedgerConnectError() } else if (e instanceof DisconnectedDeviceDuringOperation) { throw new LedgerConnectError() + } else if (e instanceof DisconnectedDevice) { + throw new LedgerConnectError() } if (error instanceof TransportStatusError) { From 8d3fc6bbdfe0558f3107d152f6df8c2284ca3774 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:20:27 -0700 Subject: [PATCH 64/73] adds owner to RpcMint (#5522) assets stored in the blockchain database include both 'creator' and 'owner', but the RpcMint type only includes 'creator' our api expects to receive mint data that includes 'owner' updates RpcMint to include owner NOTE: this field is immediately deprecated alongside 'creator' and other fields that can be read from asset RPCs. fully deprecating these fields for endpoints like 'followChainStream' and 'getTransactionStream' that expect streams of chain data could make the process of fetching asset data with separate requests between streaming responses difficult --- ironfish/src/rpc/routes/chain/followChainStream.ts | 1 + ironfish/src/rpc/routes/chain/getTransactionStream.ts | 1 + ironfish/src/rpc/routes/chain/serializers.ts | 1 + ironfish/src/rpc/routes/chain/types.ts | 5 +++++ ironfish/src/rpc/routes/wallet/mintAsset.test.ts | 1 + ironfish/src/rpc/routes/wallet/mintAsset.ts | 1 + ironfish/src/rpc/routes/wallet/serializers.ts | 1 + 7 files changed, 11 insertions(+) diff --git a/ironfish/src/rpc/routes/chain/followChainStream.ts b/ironfish/src/rpc/routes/chain/followChainStream.ts index 80d0ee63d6..8a48fd0ed7 100644 --- a/ironfish/src/rpc/routes/chain/followChainStream.ts +++ b/ironfish/src/rpc/routes/chain/followChainStream.ts @@ -103,6 +103,7 @@ routes.register = yup @@ -93,6 +97,7 @@ export const RpcMintSchema: yup.ObjectSchema = yup metadata: yup.string().defined(), name: yup.string().defined(), creator: yup.string().defined(), + owner: yup.string().defined(), assetName: yup.string().defined(), }) .defined() diff --git a/ironfish/src/rpc/routes/wallet/mintAsset.test.ts b/ironfish/src/rpc/routes/wallet/mintAsset.test.ts index 0eb4bd0be7..4fe62e8841 100644 --- a/ironfish/src/rpc/routes/wallet/mintAsset.test.ts +++ b/ironfish/src/rpc/routes/wallet/mintAsset.test.ts @@ -142,6 +142,7 @@ describe('Route wallet/mintAsset', () => { ), id: asset.id().toString('hex'), creator: asset.creator().toString('hex'), + owner: asset.creator().toString('hex'), assetId: asset.id().toString('hex'), metadata: asset.metadata().toString('hex'), hash: mintTransaction.hash().toString('hex'), diff --git a/ironfish/src/rpc/routes/wallet/mintAsset.ts b/ironfish/src/rpc/routes/wallet/mintAsset.ts index 1aed7dc9f9..aa3297a4b7 100644 --- a/ironfish/src/rpc/routes/wallet/mintAsset.ts +++ b/ironfish/src/rpc/routes/wallet/mintAsset.ts @@ -164,6 +164,7 @@ routes.register( assetName: mint.asset.name().toString('hex'), metadata: mint.asset.metadata().toString('hex'), creator: mint.asset.creator().toString('hex'), + owner: mint.asset.creator().toString('hex'), transferOwnershipTo: mint.transferOwnershipTo?.toString('hex'), }) }, diff --git a/ironfish/src/rpc/routes/wallet/serializers.ts b/ironfish/src/rpc/routes/wallet/serializers.ts index c477c27b68..bb2c27b627 100644 --- a/ironfish/src/rpc/routes/wallet/serializers.ts +++ b/ironfish/src/rpc/routes/wallet/serializers.ts @@ -79,6 +79,7 @@ export async function serializeRpcWalletTransaction( metadata: BufferUtils.toHuman(mint.asset.metadata()), name: BufferUtils.toHuman(mint.asset.name()), creator: mint.asset.creator().toString('hex'), + owner: mint.asset.creator().toString('hex'), value: mint.value.toString(), transferOwnershipTo: mint.transferOwnershipTo?.toString('hex'), assetId: mint.asset.id().toString('hex'), From 5b67a009039a2ef1cbe1d462aec112d4b0fd9b12 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Wed, 9 Oct 2024 12:49:55 -0700 Subject: [PATCH 65/73] Update readme documentation on error codes returned (#5523) --- ironfish-cli/src/ledger/README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ironfish-cli/src/ledger/README.md b/ironfish-cli/src/ledger/README.md index 34c55dc969..38ddfc627b 100644 --- a/ironfish-cli/src/ledger/README.md +++ b/ironfish-cli/src/ledger/README.md @@ -1,5 +1,8 @@ # Ledger +- Ironfish App: 0.1.0 +- Ironfish DKG App: 0.5.4 + #### IronfishApp.appInfo() (OS CLA) C APP If Dashboard Open: @@ -13,7 +16,7 @@ If Locked: throw 0x5515 DeviceLocked If Unlocked: throw 0x6e01 (APP NOT OPEN) If App Open: - If Locked: throw INS_NOT_SUPPORTED + If Locked: returns successfully If Unlocked: returns successfully ##### IronfishApp.getVersion (APP CLA) @@ -22,12 +25,12 @@ If Locked: throw 0x5515 DeviceLocked If Unlocked: throw 0x6e01 (APP NOT OPEN) If App Open: - If Locked: throw 0x5515 Device Locked + If Locked: throw 0x5515 DeviceLocked If Unlocked: returns successfully RUST APP If Dashboard Open: If Locked: throw 0x5515 DeviceLocked If Unlocked: throw 0x6e01 (APP NOT OPEN) If App Open: - If Locked: throw INS_NOT_SUPPORTED + If Locked: throw 0x5515 DeviceLocked If Unlocked: returns successfully() From 0e2738590cd45bc6bd70050357a8461c9df507ce Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Wed, 9 Oct 2024 14:34:43 -0700 Subject: [PATCH 66/73] Handle all device locks as manual retry (#5524) Because zondax removed the ability to discern between a locked app and a locked device. --- ironfish-cli/src/ledger/ledger.ts | 3 --- ironfish-cli/src/ui/ledger.ts | 9 +++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/ironfish-cli/src/ledger/ledger.ts b/ironfish-cli/src/ledger/ledger.ts index 0649309dd3..d7b91470e8 100644 --- a/ironfish-cli/src/ledger/ledger.ts +++ b/ironfish-cli/src/ledger/ledger.ts @@ -65,8 +65,6 @@ export class Ledger { if (error instanceof ResponseError) { if (error.returnCode === IronfishLedgerStatusCodes.LOCKED_DEVICE) { throw new LedgerDeviceLockedError() - } else if (error.returnCode === IronfishLedgerStatusCodes.INS_NOT_SUPPORTED) { - throw new LedgerAppLocked() } else if (error.returnCode === IronfishLedgerStatusCodes.CLA_NOT_SUPPORTED) { throw new LedgerClaNotSupportedError() } else if (error.returnCode === IronfishLedgerStatusCodes.PANIC) { @@ -171,7 +169,6 @@ export class LedgerPortIsBusyError extends LedgerError { } export class LedgerDeviceLockedError extends LedgerError {} -export class LedgerAppLocked extends LedgerError {} export class LedgerGPAuthFailed extends LedgerError {} export class LedgerClaNotSupportedError extends LedgerError {} export class LedgerAppNotOpen extends LedgerError {} diff --git a/ironfish-cli/src/ui/ledger.ts b/ironfish-cli/src/ui/ledger.ts index 52b35f117b..de6f79365d 100644 --- a/ironfish-cli/src/ui/ledger.ts +++ b/ironfish-cli/src/ui/ledger.ts @@ -15,7 +15,6 @@ import { Errors, ux } from '@oclif/core' import { Ledger, LedgerActionRejected, - LedgerAppLocked, LedgerAppNotOpen, LedgerClaNotSupportedError, LedgerConnectError, @@ -65,15 +64,15 @@ export async function ledger({ } catch (e) { clearTimeout(clearStatusTimer) - if (e instanceof LedgerAppLocked) { + if (e instanceof LedgerDeviceLockedError) { // If an app is running and it's locked, trying to poll the device // will cause the Ledger device to hide the pin screen as the user // is trying to enter their pin. When we run into this error, we // cannot send any commands to the Ledger in the app's CLA. - ux.action.stop('Ledger App Locked') + ux.action.stop('Ledger Locked') const confirmed = await ui.confirmList( - 'Ledger App Locked. Unlock and press enter to retry:', + 'Ledger Locked. Unlock and press enter to retry:', 'Retry', ) @@ -93,8 +92,6 @@ export async function ledger({ } else if (e instanceof LedgerAppNotOpen) { const appName = ledger.isMultisig ? 'Ironfish DKG' : 'Ironfish' ux.action.status = `Open Ledger App ${appName}` - } else if (e instanceof LedgerDeviceLockedError) { - ux.action.status = 'Unlock Ledger' } else if (e instanceof LedgerPanicError) { ux.action.status = 'Ledger App Crashed' ux.stdout('Ledger App Crashed! ⚠️') From 583f934fd7abf876f79713b0a24efae102211b35 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Wed, 9 Oct 2024 14:34:58 -0700 Subject: [PATCH 67/73] Add an ability to request identity based on approval (#5525) --- ironfish-cli/src/ledger/ledgerMultiSigner.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ironfish-cli/src/ledger/ledgerMultiSigner.ts b/ironfish-cli/src/ledger/ledgerMultiSigner.ts index 17ee769fd2..2057ecf5a1 100644 --- a/ironfish-cli/src/ledger/ledgerMultiSigner.ts +++ b/ironfish-cli/src/ledger/ledgerMultiSigner.ts @@ -21,8 +21,8 @@ export class LedgerMultiSigner extends Ledger { super(true) } - dkgGetIdentity = async (index: number): Promise => { - const response = await this.tryInstruction((app) => app.dkgGetIdentity(index, false)) + dkgGetIdentity = async (index: number, approval = false): Promise => { + const response = await this.tryInstruction((app) => app.dkgGetIdentity(index, approval)) return response.identity } From 2176e9cdbf7dafba32c3c5cca219fc8c37aa1eda Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Wed, 9 Oct 2024 14:36:50 -0700 Subject: [PATCH 68/73] Handle all connecting errors as LedgerConnectError (#5526) Found these in the hw-transport library from ledgerhq. --- ironfish-cli/src/ledger/ledger.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ironfish-cli/src/ledger/ledger.ts b/ironfish-cli/src/ledger/ledger.ts index d7b91470e8..ef52631a62 100644 --- a/ironfish-cli/src/ledger/ledger.ts +++ b/ironfish-cli/src/ledger/ledger.ts @@ -153,11 +153,19 @@ export class LedgerError extends Error { export class LedgerConnectError extends LedgerError { static IsError(error: unknown): error is Error { + const ids = [ + 'ListenTimeout', + 'InvalidChannel', + 'InvalidTag', + 'InvalidSequence', + 'NoDeviceFound', + ] + return ( error instanceof Error && 'id' in error && typeof error['id'] === 'string' && - error.id === 'ListenTimeout' + ids.includes(error.id) ) } } From 04d5cdbd5d0eaecbbac99436b43869d69eb2b8af Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:05:37 -0700 Subject: [PATCH 69/73] adds default multisig broker server (#5520) * adds default multisig broker server changes '--server' to boolean flag in 'wallet:multisig:sign' and 'wallet:multisig:dkg:create' adds '--hostname' and '--port' flags in place of previous '--server' flag to supply server hostname and port defaults 'hostname' to 'multisig.ironfish.network' and 'port' to 9035 adds support for connection strings with '--connection' flag adds util function to parse all connection options from flags prints connection string to console after starting session * allows sessionId and passphrase flags without server flag if a users sets the sessionId or passphrase flag in 'multisig:sign' or 'dkg:create', the commands will now interpret these flags as intent to use a broker server --- .../commands/wallet/multisig/dkg/create.ts | 47 +++++----- .../src/commands/wallet/multisig/sign.ts | 45 ++++++---- .../src/multisigBroker/clients/client.ts | 10 ++- .../src/multisigBroker/clients/tcpClient.ts | 16 ++-- .../src/multisigBroker/clients/tlsClient.ts | 2 +- ironfish-cli/src/multisigBroker/utils.ts | 85 ++++++++++++++++--- 6 files changed, 144 insertions(+), 61 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index 30dc056196..b53e2c9ace 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -44,16 +44,26 @@ export class DkgCreateCommand extends IronfishCommand { description: "Block sequence to begin scanning from for the created account. Uses node's chain head by default", }), - server: Flags.string({ - description: "multisig server to connect to. formatted as ':'", + server: Flags.boolean({ + description: 'connect to a multisig broker server', + }), + connection: Flags.string({ + char: 'c', + description: 'connection string for a multisig server session', + }), + hostname: Flags.string({ + description: 'hostname of the multisig broker server to connect to', + default: 'multisig.ironfish.network', + }), + port: Flags.integer({ + description: 'port to connect to on the multisig broker server', + default: 9035, }), sessionId: Flags.string({ description: 'Unique ID for a multisig server session to join', - dependsOn: ['server'], }), passphrase: Flags.string({ description: 'Passphrase to join the multisig server session', - dependsOn: ['server'], }), tls: Flags.boolean({ description: 'connect to the multisig server over TLS', @@ -90,21 +100,18 @@ export class DkgCreateCommand extends IronfishCommand { } let multisigClient: MultisigClient | null = null - if (flags.server) { - let sessionId = flags.sessionId - if (!sessionId && !flags.totalParticipants && !flags.minSigners) { - sessionId = await ui.inputPrompt( - 'Enter the ID of a multisig session to join, or press enter to start a new session', - false, - ) - } - - let passphrase = flags.passphrase - if (!passphrase) { - passphrase = await ui.inputPrompt('Enter the passphrase for the multisig session', true) - } - - multisigClient = await MultisigBrokerUtils.createClient(flags.server, { + if (flags.server || flags.connection || flags.sessionId || flags.passphrase) { + const { hostname, port, sessionId, passphrase } = + await MultisigBrokerUtils.parseConnectionOptions({ + connection: flags.connection, + hostname: flags.hostname, + port: flags.port, + sessionId: flags.sessionId, + passphrase: flags.passphrase, + logger: this.logger, + }) + + multisigClient = MultisigBrokerUtils.createClient(hostname, port, { passphrase, tls: flags.tls ?? true, logger: this.logger, @@ -380,6 +387,8 @@ export class DkgCreateCommand extends IronfishCommand { multisigClient.startDkgSession(totalParticipants, minSigners) this.log('\nStarted new DKG session:') this.log(`${multisigClient.sessionId}`) + this.log('\nDKG session connection string:') + this.log(`${multisigClient.connectionString}`) } return { totalParticipants, minSigners } diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts index 1d6f05b606..fda01bf01e 100644 --- a/ironfish-cli/src/commands/wallet/multisig/sign.ts +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -46,16 +46,26 @@ export class SignMultisigTransactionCommand extends IronfishCommand { default: false, description: 'Perform operation with a ledger device', }), - server: Flags.string({ - description: "multisig server to connect to. formatted as ':'", + server: Flags.boolean({ + description: 'connect to a multisig broker server', + }), + connection: Flags.string({ + char: 'c', + description: 'connection string for a multisig server session', + }), + hostname: Flags.string({ + description: 'hostname of the multisig broker server to connect to', + default: 'multisig.ironfish.network', + }), + port: Flags.integer({ + description: 'port to connect to on the multisig broker server', + default: 9035, }), sessionId: Flags.string({ description: 'Unique ID for a multisig server session to join', - dependsOn: ['server'], }), passphrase: Flags.string({ description: 'Passphrase to join the multisig server session', - dependsOn: ['server'], }), tls: Flags.boolean({ description: 'connect to the multisig server over TLS', @@ -114,21 +124,18 @@ export class SignMultisigTransactionCommand extends IronfishCommand { } let multisigClient: MultisigClient | null = null - if (flags.server) { - let sessionId = flags.sessionId - if (!sessionId) { - sessionId = await ui.inputPrompt( - 'Enter the ID of a multisig session to join, or press enter to start a new session', - false, - ) - } - - let passphrase = flags.passphrase - if (!passphrase) { - passphrase = await ui.inputPrompt('Enter the passphrase for the multisig session', true) - } + if (flags.server || flags.connection || flags.sessionId || flags.passphrase) { + const { hostname, port, sessionId, passphrase } = + await MultisigBrokerUtils.parseConnectionOptions({ + connection: flags.connection, + hostname: flags.hostname, + port: flags.port, + sessionId: flags.sessionId, + passphrase: flags.passphrase, + logger: this.logger, + }) - multisigClient = await MultisigBrokerUtils.createClient(flags.server, { + multisigClient = MultisigBrokerUtils.createClient(hostname, port, { passphrase, tls: flags.tls ?? true, logger: this.logger, @@ -267,6 +274,8 @@ export class SignMultisigTransactionCommand extends IronfishCommand { multisigClient.startSigningSession(totalParticipants, unsignedTransactionInput) this.log('\nStarted new signing session:') this.log(`${multisigClient.sessionId}`) + this.log('\nSigning session connection string:') + this.log(`${multisigClient.connectionString}`) } return { unsignedTransaction, totalParticipants } diff --git a/ironfish-cli/src/multisigBroker/clients/client.ts b/ironfish-cli/src/multisigBroker/clients/client.ts index cff5cf4fa8..413be1894c 100644 --- a/ironfish-cli/src/multisigBroker/clients/client.ts +++ b/ironfish-cli/src/multisigBroker/clients/client.ts @@ -42,6 +42,8 @@ const RETRY_INTERVAL = 5000 export abstract class MultisigClient { readonly logger: Logger readonly version: number + readonly hostname: string + readonly port: number private started: boolean private isClosing = false @@ -65,9 +67,11 @@ export abstract class MultisigClient { retries: Map = new Map() - constructor(options: { passphrase: string; logger: Logger }) { + constructor(options: { hostname: string; port: number; passphrase: string; logger: Logger }) { this.logger = options.logger this.version = 3 + this.hostname = options.hostname + this.port = options.port this.started = false this.nextMessageId = 0 @@ -78,6 +82,10 @@ export abstract class MultisigClient { this.passphrase = options.passphrase } + get connectionString(): string { + return `tcp://${this.sessionId}:${this.passphrase}@${this.hostname}:${this.port}` + } + get key(): xchacha20poly1305.XChaCha20Poly1305Key { if (!this.sessionId) { throw new Error('Client must join a session before encrypting/decrypting messages') diff --git a/ironfish-cli/src/multisigBroker/clients/tcpClient.ts b/ironfish-cli/src/multisigBroker/clients/tcpClient.ts index 18e74155b5..3de57b0423 100644 --- a/ironfish-cli/src/multisigBroker/clients/tcpClient.ts +++ b/ironfish-cli/src/multisigBroker/clients/tcpClient.ts @@ -6,15 +6,15 @@ import net from 'net' import { MultisigClient } from './client' export class MultisigTcpClient extends MultisigClient { - readonly host: string - readonly port: number - client: net.Socket | null = null - constructor(options: { host: string; port: number; passphrase: string; logger: Logger }) { - super({ passphrase: options.passphrase, logger: options.logger }) - this.host = options.host - this.port = options.port + constructor(options: { hostname: string; port: number; passphrase: string; logger: Logger }) { + super({ + hostname: options.hostname, + port: options.port, + passphrase: options.passphrase, + logger: options.logger, + }) } protected onSocketDisconnect = (): void => { @@ -50,7 +50,7 @@ export class MultisigTcpClient extends MultisigClient { client.on('error', onError) client.on('connect', onConnect) client.on('data', this.onSocketData) - client.connect({ host: this.host, port: this.port }) + client.connect({ host: this.hostname, port: this.port }) this.client = client }) } diff --git a/ironfish-cli/src/multisigBroker/clients/tlsClient.ts b/ironfish-cli/src/multisigBroker/clients/tlsClient.ts index 280a3b766b..dc4e163f03 100644 --- a/ironfish-cli/src/multisigBroker/clients/tlsClient.ts +++ b/ironfish-cli/src/multisigBroker/clients/tlsClient.ts @@ -24,7 +24,7 @@ export class MultisigTlsClient extends MultisigTcpClient { } const client = tls.connect({ - host: this.host, + host: this.hostname, port: this.port, rejectUnauthorized: false, }) diff --git a/ironfish-cli/src/multisigBroker/utils.ts b/ironfish-cli/src/multisigBroker/utils.ts index 1c4bd0435e..7fe8443f45 100644 --- a/ironfish-cli/src/multisigBroker/utils.ts +++ b/ironfish-cli/src/multisigBroker/utils.ts @@ -1,33 +1,89 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Assert, Logger, parseUrl } from '@ironfish/sdk' -import dns from 'dns' +import { ErrorUtils, Logger } from '@ironfish/sdk' +import * as ui from '../ui' import { MultisigClient, MultisigTcpClient, MultisigTlsClient } from './clients' -async function createClient( - serverAddress: string, - options: { passphrase: string; tls: boolean; logger: Logger }, -): Promise { - const parsed = parseUrl(serverAddress) +async function parseConnectionOptions(options: { + connection?: string + hostname: string + port: number + sessionId?: string + passphrase?: string + logger: Logger +}): Promise<{ + hostname: string + port: number + sessionId: string + passphrase: string +}> { + let hostname + let port + let sessionId + let passphrase + if (options.connection) { + try { + const url = new URL(options.connection) + if (url.host) { + hostname = url.hostname + } + if (url.port) { + port = Number(url.port) + } + if (url.username) { + sessionId = url.username + } + if (url.password) { + passphrase = url.password + } + } catch (e) { + if (e instanceof TypeError && e.message.includes('Invalid URL')) { + options.logger.error(ErrorUtils.renderError(e)) + } + throw e + } + } - Assert.isNotNull(parsed.hostname) - Assert.isNotNull(parsed.port) + hostname = hostname ?? options.hostname + port = port ?? options.port - const resolved = await dns.promises.lookup(parsed.hostname) - const host = resolved.address - const port = parsed.port + sessionId = sessionId ?? options.sessionId + if (!sessionId) { + sessionId = await ui.inputPrompt( + 'Enter the ID of a multisig session to join, or press enter to start a new session', + false, + ) + } + + passphrase = passphrase ?? options.passphrase + if (!passphrase) { + passphrase = await ui.inputPrompt('Enter the passphrase for the multisig session', true) + } + return { + hostname, + port, + sessionId, + passphrase, + } +} + +function createClient( + hostname: string, + port: number, + options: { passphrase: string; tls: boolean; logger: Logger }, +): MultisigClient { if (options.tls) { return new MultisigTlsClient({ - host, + hostname, port, passphrase: options.passphrase, logger: options.logger, }) } else { return new MultisigTcpClient({ - host, + hostname, port, passphrase: options.passphrase, logger: options.logger, @@ -36,5 +92,6 @@ async function createClient( } export const MultisigBrokerUtils = { + parseConnectionOptions, createClient, } From 28b2127a466160abbe5ecdbd5d3af86a19103742 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Wed, 9 Oct 2024 15:25:39 -0700 Subject: [PATCH 70/73] Upgrade ledger:backup cmd to use ledger action (#5527) --- .../commands/wallet/multisig/ledger/backup.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts b/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts index 0c1e7b4f91..7b5bf7d171 100644 --- a/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts +++ b/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts @@ -3,23 +3,20 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { IronfishCommand } from '../../../../command' import { LedgerMultiSigner } from '../../../../ledger' +import * as ui from '../../../../ui' export class MultisigLedgerBackup extends IronfishCommand { static description = `show encrypted multisig keys from a Ledger device` async start(): Promise { const ledger = new LedgerMultiSigner() - try { - await ledger.connect() - } catch (e) { - if (e instanceof Error) { - this.error(e.message) - } else { - throw e - } - } - const encryptedKeys = await ledger.dkgBackupKeys() + const encryptedKeys = await ui.ledger({ + ledger, + message: 'Getting Ledger Keys', + approval: true, + action: () => ledger.dkgBackupKeys(), + }) this.log() this.log('Encrypted Ledger Multisig Backup:') From 149adaf75ceef295192c87bcaa211982d2d47fef Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Wed, 9 Oct 2024 15:27:30 -0700 Subject: [PATCH 71/73] Fix ledger restore to require encrypted key (#5528) --- .../src/commands/wallet/multisig/ledger/restore.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts b/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts index 7251c7b8c5..a392d1bf06 100644 --- a/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts +++ b/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts @@ -19,12 +19,12 @@ export class MultisigLedgerRestore extends IronfishCommand { async start(): Promise { const { args } = await this.parse(MultisigLedgerRestore) - let encryptedKeys = args.backup - if (!encryptedKeys) { - encryptedKeys = await ui.longPrompt( + const encryptedKeys = + args.backup || + (await ui.longPrompt( 'Enter the encrypted multisig key backup to restore to your Ledger device', - ) - } + { required: true }, + )) const ledger = new LedgerMultiSigner() try { From 97d4c0cc11836cd25370bcfa063437850c470de0 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Wed, 9 Oct 2024 15:42:42 -0700 Subject: [PATCH 72/73] Upgrade ledger:restore cmd to use ledger ui (#5529) --- .../commands/wallet/multisig/ledger/restore.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts b/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts index a392d1bf06..e9dfa98e08 100644 --- a/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts +++ b/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts @@ -27,17 +27,13 @@ export class MultisigLedgerRestore extends IronfishCommand { )) const ledger = new LedgerMultiSigner() - try { - await ledger.connect() - } catch (e) { - if (e instanceof Error) { - this.error(e.message) - } else { - throw e - } - } - await ledger.dkgRestoreKeys(encryptedKeys) + await ui.ledger({ + ledger, + message: 'Restoring Keys to Ledger', + approval: true, + action: () => ledger.dkgRestoreKeys(encryptedKeys), + }) this.log() this.log('Encrypted multisig key backup restored to Ledger.') From bb17b386af7a813499ad613be23fe94dcbc7ad7c Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Wed, 9 Oct 2024 16:06:59 -0700 Subject: [PATCH 73/73] Bump SDK and CLI to 2.8.0 (#5532) --- ironfish-cli/package.json | 4 ++-- ironfish/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index 944ca5e887..c283c7bad0 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -1,6 +1,6 @@ { "name": "ironfish", - "version": "2.7.0", + "version": "2.8.0", "description": "CLI for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js", @@ -61,7 +61,7 @@ }, "dependencies": { "@ironfish/rust-nodejs": "2.7.0", - "@ironfish/sdk": "2.7.0", + "@ironfish/sdk": "2.8.0", "@ledgerhq/errors": "6.17.0", "@ledgerhq/hw-transport-node-hid": "6.29.1", "@oclif/core": "4.0.11", diff --git a/ironfish/package.json b/ironfish/package.json index 17494b0aba..794377a55a 100644 --- a/ironfish/package.json +++ b/ironfish/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/sdk", - "version": "2.7.0", + "version": "2.8.0", "description": "SDK for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js",