Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement wallet/multisig/dkg/round1 RPC #4885

Merged
merged 2 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
31 changes: 31 additions & 0 deletions ironfish-rust-nodejs/src/multisig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,3 +353,34 @@ 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)?;

let (encrypted_secret_package, public_package) = ironfish_frost::dkg::round1::round1(
&self_identity,
min_signers,
&participant_identities,
thread_rng(),
)
.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'
121 changes: 121 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,121 @@
/* 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 { Assert } from '../../../../../assert'
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),
andiflabs marked this conversation as resolved.
Show resolved Hide resolved
publicPackage: expect.any(String),
})

// Ensure that the encrypted secret package can be decrypted
const secretValue = await routeTest.node.wallet.walletDb.getMultisigSecretByName(secretName)
Assert.isNotUndefined(secretValue)
const secret = new multisig.ParticipantSecret(secretValue.secret)
secret.decryptData(Buffer.from(response.content.encryptedSecretPackage, 'hex'))
})

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,
)
}
andiflabs marked this conversation as resolved.
Show resolved Hide resolved

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'
Loading