Skip to content

Commit

Permalink
feat: DKG Create Command
Browse files Browse the repository at this point in the history
- Implemented the multisig:dkg:create cli command to facilitate the creation of multisignature accounts using Distributed Key Generation (DKG).
- Added methods for participant management, and DKG rounds (1, 2, and 3) with user prompts for input.
- Integrated error handling and retry logic for every step.
- Enhanced user experience with informative logging and prompts throughout the process.
  • Loading branch information
patnir committed Sep 20, 2024
1 parent a487c5d commit 9574636
Show file tree
Hide file tree
Showing 3 changed files with 242 additions and 2 deletions.
235 changes: 235 additions & 0 deletions ironfish-cli/src/commands/wallet/multisig/dkg/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
/* 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 { RPC_ERROR_CODES, RpcClient, RpcRequestError } from '@ironfish/sdk'
import { Flags } from '@oclif/core'
import { IronfishCommand } from '../../../../command'
import { RemoteFlags } from '../../../../flags'
import * as ui from '../../../../ui'

export class DkgCreateCommand extends IronfishCommand {
static description = 'Interactive command to create a multisignature account using DKG'

static flags = {
...RemoteFlags,
participantName: Flags.string({
char: 'n',
description: 'The name of the participant to use for DKG',
}),
accountName: Flags.string({
char: 'a',
description: 'The name to set for the imported account',
}),
}

async start(): Promise<void> {
const { flags } = await this.parse(DkgCreateCommand)
const client = await this.connectRpc()
await ui.checkWalletUnlocked(client)

const participantName = flags.participantName
const accountName = flags.accountName

const { name, identity } = await this.retryStep('Get or create participant', async () => {
return this.getOrCreateParticipant(client, participantName)
})

const { round1Result } = await this.retryStep(
'Collect participant info and perform Round 1',
async () => {
return this.performRound1(client, name, identity)
},
)

const { round2Result, round1PublicPackages } = await this.retryStep(
'Collect Round 1 public packages and perform Round 2',
async () => {
return this.performRound2(client, name, round1Result)
},
)

await this.retryStep('Perform Round 3', async () => {
await this.performRound3(client, name, accountName, round2Result, round1PublicPackages)
})

this.log('DKG process completed successfully!')
}

private async retryStep<T>(stepName: string, stepFunction: () => Promise<T>): Promise<T> {
// eslint-disable-next-line no-constant-condition
while (true) {
try {
this.log(`\nStarting step: ${stepName}`)
const result = await stepFunction()
this.log(`Step completed: ${stepName}`)
return result
} catch (error) {
this.logger.log(`An error occurred during ${stepName}: ${(error as Error).message}`)
await ui.confirmOrQuit('Do you want to retry this step?')
}
}
}

async getOrCreateParticipant(
client: RpcClient,
name?: string,
): Promise<{ name: string; identity: string }> {
if (!name) {
name = await ui.inputPrompt('Enter a name for your participant identity', true)
}

let identity: string

try {
identity = (await client.wallet.multisig.getIdentity({ name })).content.identity
this.log(`Using existing participant: ${name}`)
} catch (error) {
if (
error instanceof RpcRequestError &&
error.code === RPC_ERROR_CODES.IDENTITY_NOT_FOUND.toString()
) {
this.log(`Creating new participant: ${name}`)
identity = (await client.wallet.multisig.createParticipant({ name })).content.identity
} else {
throw error
}
}

this.log(`Participant identity: ${identity} for ${name}`)

return { name, identity }
}

async performRound1(
client: RpcClient,
participantName: string,
currentIdentity: string,
): Promise<{ round1Result: { secretPackage: string; publicPackage: string } }> {
this.log('\nCollecting Participant Info and Performing Round 1...')

let input = await ui.inputPrompt('Enter the total number of participants', true)
const totalParticipants = parseInt(input)
if (isNaN(totalParticipants) || totalParticipants < 2) {
throw new Error('Total number of participants must be at least 2')
}

input = await ui.longPrompt(
'Enter the identities of all other participants (excluding yours), separated by commas',
{ required: true },
)
const inputIdentities = input.split(',').map((i) => i.trim())

if (inputIdentities.length !== totalParticipants - 1) {
throw new Error(
`Number of input identities must be ${totalParticipants - 1}, ${
inputIdentities.length
} provided`,
)
}

input = await ui.inputPrompt('Enter the number of minimum signers', true)
const minSigners = parseInt(input)
if (isNaN(minSigners) || minSigners < 2) {
throw new Error('Minimum number of signers must be at least 2')
}

const identities = [...inputIdentities, currentIdentity]

this.log('\nPerforming DKG Round 1...')
const response = await client.wallet.multisig.dkg.round1({
participantName,
participants: identities.map((identity) => ({ identity })),
minSigners,
})

this.log('\nRound 1 Encrypted Secret Package:')
this.log(response.content.round1SecretPackage)

this.log('\nRound 1 Public Package:')
this.log(response.content.round1PublicPackage)

this.log('\nShare your Round 1 Public Package with other participants.')
return {
round1Result: {
secretPackage: response.content.round1SecretPackage,
publicPackage: response.content.round1PublicPackage,
},
}
}

async performRound2(
client: RpcClient,
participantName: string,
round1Result: { secretPackage: string; publicPackage: string },
): Promise<{
round2Result: { secretPackage: string; publicPackage: string }
round1PublicPackages: string[]
}> {
this.log('\nCollecting Round 1 Public Packages...')
const input = await ui.longPrompt(
"Enter all other participants' (excluding yours) Round 1 Public Packages, separated by commas",
{ required: true },
)
const otherPackages = input.split(',').map((p) => p.trim())
const round1PublicPackages = [...otherPackages, round1Result.publicPackage]

this.log('\nPerforming DKG Round 2...')

const response = await client.wallet.multisig.dkg.round2({
participantName,
round1SecretPackage: round1Result.secretPackage,
round1PublicPackages,
})

this.log('\nRound 2 Encrypted Secret Package:')
this.log(response.content.round2SecretPackage)

this.log('\nRound 2 Public Package:')
this.log(response.content.round2PublicPackage)

this.log('\nShare your Round 2 Public Package with other participants.')

return {
round2Result: {
secretPackage: response.content.round2SecretPackage,
publicPackage: response.content.round2PublicPackage,
},
round1PublicPackages,
}
}

async performRound3(
client: RpcClient,
participantName: string,
accountName: string | undefined,
round2Result: { secretPackage: string; publicPackage: string },
round1PublicPackages: string[],
): Promise<void> {
this.log('\nPerforming DKG Round 3...')

if (!accountName) {
accountName = await ui.inputPrompt('Enter a name for the multisig account', true)
}

const round2PublicPackagesInput = await ui.longPrompt(
'Enter all Round 2 Public Packages (excluding yours), separated by commas',
{ required: true },
)

const round2PublicPackages = round2PublicPackagesInput.split(',').map((p) => p.trim())
round2PublicPackages.push(round2Result.publicPackage)

const response = await client.wallet.multisig.dkg.round3({
participantName,
accountName,
round2SecretPackage: round2Result.secretPackage,
round1PublicPackages,
round2PublicPackages,
})

this.log('\nMultisig account created successfully!')
this.log(`Account Name: ${response.content.name}`)
this.log(`Public Address: ${response.content.publicAddress}`)
}
}
1 change: 1 addition & 0 deletions ironfish/src/rpc/adapters/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export enum RPC_ERROR_CODES {
INSUFFICIENT_BALANCE = 'insufficient-balance',
UNAUTHENTICATED = 'unauthenticated',
NOT_FOUND = 'not-found',
IDENTITY_NOT_FOUND = 'identity-not-found',
DUPLICATE_ACCOUNT_NAME = 'duplicate-account-name',
DUPLICATE_IDENTITY_NAME = 'duplicate-identity-name',
IMPORT_ACCOUNT_NAME_REQUIRED = 'import-account-name-required',
Expand Down
8 changes: 6 additions & 2 deletions ironfish/src/rpc/routes/wallet/multisig/getIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 * as yup from 'yup'
import { RpcValidationError } from '../../../adapters/errors'
import { RPC_ERROR_CODES, RpcValidationError } from '../../../adapters/errors'
import { ApiNamespace } from '../../namespaces'
import { routes } from '../../router'
import { AssertHasRpcContext } from '../../rpcContext'
Expand Down Expand Up @@ -37,7 +37,11 @@ routes.register<typeof GetIdentityRequestSchema, GetIdentityResponse>(

const identity = await context.wallet.walletDb.getMultisigIdentityByName(name)
if (identity === undefined) {
throw new RpcValidationError(`No identity found with name ${name}`, 404)
throw new RpcValidationError(
`No identity found with name ${name}`,
404,
RPC_ERROR_CODES.IDENTITY_NOT_FOUND,
)
}

request.end({ identity: identity.toString('hex') })
Expand Down

0 comments on commit 9574636

Please sign in to comment.