Skip to content

Commit

Permalink
Implement wallet/multisig/dkg/round1 RPC
Browse files Browse the repository at this point in the history
  • Loading branch information
andiflabs committed Apr 9, 2024
1 parent 51cee64 commit fe43358
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 3 deletions.
5 changes: 5 additions & 0 deletions ironfish-rust-nodejs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,11 @@ export namespace multisig {
proofAuthorizingKey: string
keyPackages: Array<ParticipantKeyPackage>
}
export function dkgRound1(selfIdentity: string, minSigners: number, participantIdentities: Array<string>): DkgRound1Packages
export interface DkgRound1Packages {
encryptedSecretPackage: string
publicPackage: string
}
export function aggregateSignatureShares(publicKeyPackageStr: string, signingPackageStr: string, signatureSharesArr: Array<string>): Buffer
export class ParticipantSecret {
constructor(jsBytes: Buffer)
Expand Down
61 changes: 58 additions & 3 deletions ironfish-rust-nodejs/src/multisig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@

use crate::{structs::NativeUnsignedTransaction, to_napi_err};
use ironfish::{
frost::{keys::KeyPackage, round2, Randomizer},
frost::{
keys::{dkg, KeyPackage},
round2, Randomizer,
},
frost_utils::{signing_package::SigningPackage, split_spender_key::split_spender_key},
participant::{Identity, Secret},
serializing::{bytes_to_hex, fr::FrSerializable, hex_to_vec_bytes},
SaplingKey,
};
use ironfish_frost::{
keys::PublicKeyPackage, multienc, nonces::deterministic_signing_nonces,
signature_share::SignatureShare, signing_commitment::SigningCommitment,
dkg::round1::export_secret_package, keys::PublicKeyPackage, multienc,
nonces::deterministic_signing_nonces, signature_share::SignatureShare,
signing_commitment::SigningCommitment,
};
use napi::{bindgen_prelude::*, JsBuffer};
use napi_derive::napi;
Expand Down Expand Up @@ -353,3 +357,54 @@ impl NativeSigningPackage {
.collect()
}
}

#[napi(namespace = "multisig")]
pub fn dkg_round1(
self_identity: String,
min_signers: u16,
participant_identities: Vec<String>,
) -> Result<DkgRound1Packages> {
let self_identity =
Identity::deserialize_from(&hex_to_vec_bytes(&self_identity).map_err(to_napi_err)?[..])?;
let participant_identities = try_deserialize_identities(participant_identities)?;

if !participant_identities.contains(&self_identity) {
return Err(to_napi_err(
"participant_identities must include self_identity",
));
}

let max_signers = u16::try_from(participant_identities.len())
.map_err(|_| to_napi_err("too many participants"))?;

if min_signers > max_signers {
return Err(to_napi_err(
"min_signers exceeds the number of participants",
));
}

let mut rng = thread_rng();
let (secret_package, public_package) = dkg::part1(
self_identity.to_frost_identifier(),
max_signers,
min_signers,
&mut rng,
)
.map_err(to_napi_err)?;

let encrypted_secret_package =
export_secret_package(&secret_package, &self_identity, &mut rng)?;
let public_package = public_package.serialize().map_err(to_napi_err)?;

// TODO bind the min/max signers and the list of participants to the packages
Ok(DkgRound1Packages {
encrypted_secret_package: bytes_to_hex(&encrypted_secret_package),
public_package: bytes_to_hex(&public_package),
})
}

#[napi(object, namespace = "multisig")]
pub struct DkgRound1Packages {
pub encrypted_secret_package: String,
pub public_package: String,
}
11 changes: 11 additions & 0 deletions ironfish/src/rpc/clients/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import type {
CreateTransactionResponse,
CreateTrustedDealerKeyPackageRequest,
CreateTrustedDealerKeyPackageResponse,
DkgRound1Request,
DkgRound1Response,
EstimateFeeRateRequest,
EstimateFeeRateResponse,
EstimateFeeRatesRequest,
Expand Down Expand Up @@ -271,6 +273,15 @@ export abstract class RpcClient {
params,
).waitForEnd()
},

dkg: {
round1: (params: DkgRound1Request): Promise<RpcResponseEnded<DkgRound1Response>> => {
return this.request<DkgRound1Response>(
`${ApiNamespace.wallet}/multisig/dkg/round1`,
params,
).waitForEnd()
},
},
},

getAccounts: (
Expand Down
4 changes: 4 additions & 0 deletions ironfish/src/rpc/routes/wallet/multisig/dkg/index.ts
Original file line number Diff line number Diff line change
@@ -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 './round1'
114 changes: 114 additions & 0 deletions ironfish/src/rpc/routes/wallet/multisig/dkg/round1.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/* 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 { multisig } from '@ironfish/rust-nodejs'
import { createRouteTest } from '../../../../../testUtilities/routeTest'

describe('Route multisig/dkg/round1', () => {
const routeTest = createRouteTest()

it('should create round 1 packages', async () => {
const secretName = 'name'
await routeTest.client.wallet.multisig.createParticipant({ name: secretName })

const identity = (await routeTest.client.wallet.multisig.getIdentity({ name: secretName }))
.content.identity
const otherParticipants = Array.from({ length: 2 }, () => ({
identity: multisig.ParticipantSecret.random().toIdentity().serialize().toString('hex'),
}))
const participants = [{ identity }, ...otherParticipants]

const request = { secretName, minSigners: 2, participants }
const response = await routeTest.client.wallet.multisig.dkg.round1(request)

expect(response.content).toMatchObject({
encryptedSecretPackage: expect.any(String),
publicPackage: expect.any(String),
})
})

it('should fail if the named secret does not exist', async () => {
const secretName = 'name'
await routeTest.client.wallet.multisig.createParticipant({ name: secretName })

const identity = (await routeTest.client.wallet.multisig.getIdentity({ name: secretName }))
.content.identity
const otherParticipants = Array.from({ length: 2 }, () => ({
identity: multisig.ParticipantSecret.random().toIdentity().serialize().toString('hex'),
}))
const participants = [{ identity }, ...otherParticipants]

const request = { secretName: 'otherName', minSigners: 2, participants }

await expect(routeTest.client.wallet.multisig.dkg.round1(request)).rejects.toThrow(
expect.objectContaining({
message: expect.stringContaining("Multisig secret with name 'otherName' not found"),
status: 400,
}),
)
})

it('should fail if the named identity is not part of the participants', async () => {
const secretName = 'name'
await routeTest.client.wallet.multisig.createParticipant({ name: secretName })

const participants = Array.from({ length: 3 }, () => ({
identity: multisig.ParticipantSecret.random().toIdentity().serialize().toString('hex'),
}))

const request = { secretName, minSigners: 2, participants }

await expect(routeTest.client.wallet.multisig.dkg.round1(request)).rejects.toThrow(
expect.objectContaining({
message: expect.stringContaining(
"List of participant identities must include the identity for 'name'",
),
status: 400,
}),
)
})

it('should fail if minSigners is too low', async () => {
const secretName = 'name'
await routeTest.client.wallet.multisig.createParticipant({ name: secretName })

const identity = (await routeTest.client.wallet.multisig.getIdentity({ name: secretName }))
.content.identity
const otherParticipants = Array.from({ length: 2 }, () => ({
identity: multisig.ParticipantSecret.random().toIdentity().serialize().toString('hex'),
}))
const participants = [{ identity }, ...otherParticipants]

const request = { secretName, minSigners: 1, participants }

await expect(routeTest.client.wallet.multisig.dkg.round1(request)).rejects.toThrow(
expect.objectContaining({
message: expect.stringContaining('minSigners must be 2 or greater, got 1'),
status: 400,
}),
)
})

it('should fail if minSigners exceeds the number of participants', async () => {
const secretName = 'name'
await routeTest.client.wallet.multisig.createParticipant({ name: secretName })

const identity = (await routeTest.client.wallet.multisig.getIdentity({ name: secretName }))
.content.identity
const otherParticipants = Array.from({ length: 2 }, () => ({
identity: multisig.ParticipantSecret.random().toIdentity().serialize().toString('hex'),
}))
const participants = [{ identity }, ...otherParticipants]

const request = { secretName, minSigners: 4, participants }

await expect(routeTest.client.wallet.multisig.dkg.round1(request)).rejects.toThrow(
expect.objectContaining({
message: expect.stringContaining(
'minSigners (4) exceeds the number of participants (3)',
),
status: 400,
}),
)
})
})
91 changes: 91 additions & 0 deletions ironfish/src/rpc/routes/wallet/multisig/dkg/round1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/* 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 { multisig } from '@ironfish/rust-nodejs'
import * as yup from 'yup'
import { RPC_ERROR_CODES, RpcValidationError } from '../../../../adapters'
import { ApiNamespace } from '../../../namespaces'
import { routes } from '../../../router'
import { AssertHasRpcContext } from '../../../rpcContext'

export type DkgRound1Request = {
secretName: string
minSigners: number
participants: Array<{ identity: string }>
}

export type DkgRound1Response = {
encryptedSecretPackage: string
publicPackage: string
}

export const DkgRound1RequestSchema: yup.ObjectSchema<DkgRound1Request> = yup
.object({
secretName: yup.string().defined(),
minSigners: yup.number().defined(),
participants: yup
.array()
.of(yup.object({ identity: yup.string().defined() }).defined())
.defined(),
})
.defined()

export const DkgRound1ResponseSchema: yup.ObjectSchema<DkgRound1Response> = yup
.object({
encryptedSecretPackage: yup.string().defined(),
publicPackage: yup.string().defined(),
})
.defined()

routes.register<typeof DkgRound1RequestSchema, DkgRound1Response>(
`${ApiNamespace.wallet}/multisig/dkg/round1`,
DkgRound1RequestSchema,
async (request, node): Promise<void> => {
AssertHasRpcContext(request, node, 'wallet')

const { secretName, minSigners, participants } = request.data
const multisigSecret = await node.wallet.walletDb.getMultisigSecretByName(secretName)

if (!multisigSecret) {
throw new RpcValidationError(
`Multisig secret with name '${secretName}' not found`,
400,
RPC_ERROR_CODES.MULTISIG_SECRET_NOT_FOUND,
)
}

const participantIdentities = participants.map((p) => p.identity)
const selfIdentity = new multisig.ParticipantSecret(multisigSecret.secret)
.toIdentity()
.serialize()
.toString('hex')

if (!participantIdentities.includes(selfIdentity)) {
throw new RpcValidationError(
`List of participant identities must include the identity for '${secretName}' (${selfIdentity})`,
400,
RPC_ERROR_CODES.VALIDATION,
)
}

if (minSigners < 2) {
throw new RpcValidationError(
`minSigners must be 2 or greater, got ${minSigners}`,
400,
RPC_ERROR_CODES.VALIDATION,
)
}

if (minSigners > participantIdentities.length) {
throw new RpcValidationError(
`minSigners (${minSigners}) exceeds the number of participants (${participantIdentities.length})`,
400,
RPC_ERROR_CODES.VALIDATION,
)
}

const packages = multisig.dkgRound1(selfIdentity, minSigners, participantIdentities)

request.end(packages)
},
)
1 change: 1 addition & 0 deletions ironfish/src/rpc/routes/wallet/multisig/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './createParticipant'
export * from './getIdentity'
export * from './getIdentities'
export * from './getAccountIdentities'
export * from './dkg'

0 comments on commit fe43358

Please sign in to comment.