diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9a86045439..dd60152c20 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -14,8 +14,7 @@ related documentation pull request for the website. ## Breaking Change -Is this a breaking change? If yes, add notes below on why this is breaking and -what additional work is required, if any. +Is this a breaking change? If yes, add notes below on why this is breaking and label it with `breaking-change-rpc` or `breaking-change-sdk`. ``` [ ] Yes diff --git a/Cargo.lock b/Cargo.lock index 0fbccbc2c7..58d067a89c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1521,7 +1521,7 @@ dependencies = [ [[package]] name = "ironfish-frost" version = "0.1.0" -source = "git+https://github.com/iron-fish/ironfish-frost.git?branch=main#18b413c75df23f9460bd143fb2d044632ef9d1c9" +source = "git+https://github.com/iron-fish/ironfish-frost.git?branch=main#cabc4474446df9742e677d142520aecf790bb472" dependencies = [ "blake3", "chacha20 0.9.1", @@ -1531,6 +1531,7 @@ dependencies = [ "rand_chacha", "rand_core", "reddsa 0.5.1", + "siphasher", "x25519-dalek", ] @@ -2611,6 +2612,12 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fe458c98333f9c8152221191a77e2a44e8325d0193484af2e9421a53019e57d" +[[package]] +name = "siphasher" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54ac45299ccbd390721be55b412d41931911f654fa99e2cb8bfb57184b2061fe" + [[package]] name = "slab" version = "0.4.8" diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index a3389e1160..294adb1bdd 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -1,6 +1,6 @@ { "name": "ironfish", - "version": "2.0.0", + "version": "2.1.0", "description": "CLI for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js", @@ -62,8 +62,8 @@ "@aws-sdk/client-s3": "3", "@aws-sdk/client-secrets-manager": "3", "@aws-sdk/s3-request-presigner": "3", - "@ironfish/rust-nodejs": "2.0.0", - "@ironfish/sdk": "2.0.0", + "@ironfish/rust-nodejs": "2.1.0", + "@ironfish/sdk": "2.1.0", "@oclif/core": "1.23.1", "@oclif/plugin-help": "5.1.12", "@oclif/plugin-not-found": "2.3.1", diff --git a/ironfish-cli/src/commands/wallet/import.ts b/ironfish-cli/src/commands/wallet/import.ts index 16c90e484c..e1138005e5 100644 --- a/ironfish-cli/src/commands/wallet/import.ts +++ b/ironfish-cli/src/commands/wallet/import.ts @@ -92,20 +92,15 @@ export class ImportCommand extends IronfishCommand { if ( e instanceof RpcRequestError && (e.code === RPC_ERROR_CODES.DUPLICATE_ACCOUNT_NAME.toString() || - e.code === RPC_ERROR_CODES.IMPORT_ACCOUNT_NAME_REQUIRED.toString() || - e.code === RPC_ERROR_CODES.MULTISIG_SECRET_NAME_REQUIRED.toString()) + e.code === RPC_ERROR_CODES.IMPORT_ACCOUNT_NAME_REQUIRED.toString()) ) { - let message = 'Enter a name for the account' + const message = 'Enter a name for the account' if (e.code === RPC_ERROR_CODES.DUPLICATE_ACCOUNT_NAME.toString()) { this.log() this.log(e.codeMessage) } - if (e.code === RPC_ERROR_CODES.MULTISIG_SECRET_NAME_REQUIRED.toString()) { - message = 'Enter the name of the multisig secret' - } - const name = await CliUx.ux.prompt(message, { required: true, }) diff --git a/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts b/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts index b29f012a01..e71f8091ce 100644 --- a/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts +++ b/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts @@ -6,10 +6,10 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' import { longPrompt } from '../../../../utils/longPrompt' +import { MultisigTransactionJson } from '../../../../utils/multisig' export class CreateSigningPackage extends IronfishCommand { static description = `Creates a signing package for a given transaction for a multisig account` - static hidden = true static flags = { ...RemoteFlags, @@ -28,20 +28,25 @@ export class CreateSigningPackage extends IronfishCommand { 'The signing commitments from participants to be used for creating the signing package', multiple: true, }), + path: Flags.string({ + description: 'Path to a JSON file containing multisig transaction data', + }), } async start(): Promise { const { flags } = await this.parse(CreateSigningPackage) - let unsignedTransaction = flags.unsignedTransaction?.trim() + const loaded = await MultisigTransactionJson.load(this.sdk.fileSystem, flags.path) + const options = MultisigTransactionJson.resolveFlags(flags, loaded) + let unsignedTransaction = options.unsignedTransaction if (!unsignedTransaction) { unsignedTransaction = await longPrompt('Enter the unsigned transaction', { required: true, }) } - let commitments = flags.commitment + let commitments = options.commitment if (!commitments) { const input = await longPrompt('Enter the signing commitments separated by commas', { required: true, @@ -60,5 +65,9 @@ export class CreateSigningPackage extends IronfishCommand { this.log(`Signing Package for commitments from ${commitments.length} participants:\n`) this.log(signingPackageResponse.content.signingPackage) + + this.log() + this.log('Next step:') + this.log('Send the signing package to all of the participants who provided a commitment.') } } diff --git a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts index 7b0fd1694c..09f29030e2 100644 --- a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts @@ -6,11 +6,11 @@ import { CliUx, Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' import { longPrompt } from '../../../../utils/longPrompt' +import { MultisigTransactionJson } from '../../../../utils/multisig' import { renderUnsignedTransactionDetails } from '../../../../utils/transaction' export class CreateSigningCommitmentCommand extends IronfishCommand { static description = 'Create a signing commitment from a participant for a given transaction' - static hidden = true static flags = { ...RemoteFlags, @@ -34,12 +34,18 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { default: false, description: 'Confirm creating signing commitment without confirming', }), + path: Flags.string({ + description: 'Path to a JSON file containing multisig transaction data', + }), } async start(): Promise { const { flags } = await this.parse(CreateSigningCommitmentCommand) - let identities = flags.identity + const loaded = await MultisigTransactionJson.load(this.sdk.fileSystem, flags.path) + const options = MultisigTransactionJson.resolveFlags(flags, loaded) + + let identities = options.identity if (!identities || identities.length < 2) { const input = await longPrompt('Enter the identities separated by commas', { required: true, @@ -52,7 +58,7 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { } identities = identities.map((i) => i.trim()) - let unsignedTransactionInput = flags.unsignedTransaction?.trim() + let unsignedTransactionInput = options.unsignedTransaction if (!unsignedTransactionInput) { unsignedTransactionInput = await longPrompt('Enter the unsigned transaction', { required: true, @@ -86,5 +92,9 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { this.log('\nCommitment:\n') this.log(response.content.commitment) + + this.log() + this.log('Next step:') + this.log('Send the commitment to the multisig account coordinator.') } } diff --git a/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts b/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts index db3346b029..1dcbba8214 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts @@ -1,10 +1,11 @@ /* 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 } from '@ironfish/sdk' +import { ACCOUNT_SCHEMA_VERSION, RpcClient } from '@ironfish/sdk' import { CliUx, Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' +import { longPrompt } from '../../../../utils/longPrompt' export class MultisigCreateDealer extends IronfishCommand { static description = `Create a set of multisig accounts from participant identities` @@ -37,7 +38,7 @@ export class MultisigCreateDealer extends IronfishCommand { let identities = flags.identity if (!identities || identities.length < 2) { - const input = await CliUx.ux.prompt('Enter the identities separated by commas', { + const input = await longPrompt('Enter the identities separated by commas', { required: true, }) identities = input.split(',') @@ -59,12 +60,10 @@ export class MultisigCreateDealer extends IronfishCommand { } } - const name = - flags.name?.trim() ?? - (await CliUx.ux.prompt('Enter the name for the coordinator', { required: true })) - const client = await this.sdk.connectRpc() + const name = await this.getCoordinatorName(client, flags.name?.trim()) + const response = await client.wallet.multisig.createTrustedDealerKeyPackage({ minSigners, participants: identities.map((identity) => ({ identity })), @@ -114,5 +113,33 @@ export class MultisigCreateDealer extends IronfishCommand { } this.log() + this.log('Next step:') + this.log('Send the account imports to the participant with the corresponding identity.') + } + + async getCoordinatorName(client: RpcClient, inputName?: string): Promise { + const accountsResponse = await client.wallet.getAccounts() + const accountNames = new Set(accountsResponse.content.accounts) + + const identitiesResponse = await client.wallet.multisig.getIdentities() + const secretNames = new Set(identitiesResponse.content.identities.map((i) => i.name)) + + let name = inputName + do { + name = + name ?? (await CliUx.ux.prompt('Enter a name for the coordinator', { required: true })) + + if (accountNames.has(name)) { + this.log(`Account with name ${name} already exists`) + this.log('') + name = undefined + } else if (secretNames.has(name)) { + this.log(`Multisig identity with name ${name} already exists`) + this.log('') + name = undefined + } + } while (name === undefined) + + return name } } diff --git a/ironfish-cli/src/commands/wallet/multisig/participant/create.ts b/ironfish-cli/src/commands/wallet/multisig/participant/create.ts index 89fbef5941..fb4c72502e 100644 --- a/ironfish-cli/src/commands/wallet/multisig/participant/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/participant/create.ts @@ -8,7 +8,6 @@ import { RemoteFlags } from '../../../../flags' export class MultisigIdentityCreate extends IronfishCommand { static description = `Create a multisig participant identity` - static hidden = true static flags = { ...RemoteFlags, @@ -39,15 +38,20 @@ export class MultisigIdentityCreate extends IronfishCommand { ) { this.log() this.log(e.codeMessage) + name = await CliUx.ux.prompt('Enter a new name for the identity', { + required: true, + }) + } else { + throw e } - - name = await CliUx.ux.prompt('Enter a new name for the identity', { - required: true, - }) } } this.log('Identity:') this.log(response.content.identity) + + this.log() + this.log('Next step:') + this.log('Send the identity to the multisig account dealer.') } } diff --git a/ironfish-cli/src/commands/wallet/multisig/participant/index.ts b/ironfish-cli/src/commands/wallet/multisig/participant/index.ts index 47de09b312..7fa6698989 100644 --- a/ironfish-cli/src/commands/wallet/multisig/participant/index.ts +++ b/ironfish-cli/src/commands/wallet/multisig/participant/index.ts @@ -2,19 +2,18 @@ * 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 inquirer from 'inquirer' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' export class MultisigIdentity extends IronfishCommand { static description = `Retrieve a multisig participant identity from a name` - static hidden = true static flags = { ...RemoteFlags, name: Flags.string({ char: 'n', description: 'Name of the participant identity', - required: true, }), } @@ -23,9 +22,38 @@ export class MultisigIdentity extends IronfishCommand { const client = await this.sdk.connectRpc() - const response = await client.wallet.multisig.getIdentity({ name: flags.name }) + if (flags.name) { + const response = await client.wallet.multisig.getIdentity({ name: flags.name }) - this.log('Identity:') - this.log(response.content.identity) + this.log('Identity:') + this.log(response.content.identity) + } else { + const response = await client.wallet.multisig.getIdentities() + + const choices = [] + for (const { name, identity } of response.content.identities) { + choices.push({ + name, + value: identity, + }) + } + + // sort identities by name + choices.sort((a, b) => a.name.localeCompare(b.name)) + + const selection = await inquirer.prompt<{ + identity: string + }>([ + { + name: 'identity', + message: 'Select participant name to view identity', + type: 'list', + choices, + }, + ]) + + this.log('Identity:') + this.log(selection.identity) + } } } diff --git a/ironfish-cli/src/commands/wallet/multisig/participants/index.ts b/ironfish-cli/src/commands/wallet/multisig/participants/index.ts index 346d4f4b2f..7f6af9cfb9 100644 --- a/ironfish-cli/src/commands/wallet/multisig/participants/index.ts +++ b/ironfish-cli/src/commands/wallet/multisig/participants/index.ts @@ -7,7 +7,6 @@ import { RemoteFlags } from '../../../../flags' export class MultisigAccountParticipants extends IronfishCommand { static description = `List the participant identities for a multisig account` - static hidden = true static flags = { ...RemoteFlags, diff --git a/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts b/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts index 0718840a49..21e6dd88f2 100644 --- a/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts +++ b/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts @@ -6,10 +6,11 @@ import { CliUx, Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' import { longPrompt } from '../../../../utils/longPrompt' +import { MultisigTransactionJson } from '../../../../utils/multisig' +import { watchTransaction } from '../../../../utils/transaction' export class MultisigSign extends IronfishCommand { static description = 'Aggregate signature shares from participants to sign a transaction' - static hidden = true static flags = { ...RemoteFlags, @@ -32,16 +33,27 @@ export class MultisigSign extends IronfishCommand { allowNo: true, description: 'Broadcast the transaction to the network after signing', }), + watch: Flags.boolean({ + default: false, + description: 'Wait for the transaction to be confirmed', + }), + path: Flags.string({ + description: 'Path to a JSON file containing multisig transaction data', + }), } async start(): Promise { const { flags } = await this.parse(MultisigSign) - const signingPackage = - flags.signingPackage?.trim() ?? - (await longPrompt('Enter the signing package', { required: true })) + const loaded = await MultisigTransactionJson.load(this.sdk.fileSystem, flags.path) + const options = MultisigTransactionJson.resolveFlags(flags, loaded) + + let signingPackage = options.signingPackage + if (!signingPackage) { + signingPackage = await longPrompt('Enter the signing package', { required: true }) + } - let signatureShares = flags.signatureShare + let signatureShares = options.signatureShare if (!signatureShares) { const input = await longPrompt('Enter the signature shares separated by commas', { required: true, @@ -79,5 +91,16 @@ export class MultisigSign extends IronfishCommand { this.log(`Transaction: ${response.content.transaction}`) this.log(`Hash: ${transaction.hash().toString('hex')}`) this.log(`Fee: ${CurrencyUtils.renderIron(transaction.fee(), true)}`) + + if (flags.watch) { + this.log('') + + await watchTransaction({ + client, + logger: this.logger, + account: flags.account, + hash: transaction.hash().toString('hex'), + }) + } } } diff --git a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts index dab710dfe1..ddf975ca32 100644 --- a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts @@ -1,16 +1,17 @@ /* 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 { UnsignedTransaction } from '@ironfish/sdk' import { CliUx, Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' import { longPrompt } from '../../../../utils/longPrompt' +import { MultisigTransactionJson } from '../../../../utils/multisig' import { renderUnsignedTransactionDetails } from '../../../../utils/transaction' export class CreateSignatureShareCommand extends IronfishCommand { static description = `Creates a signature share for a participant for a given transaction` - static hidden = true static flags = { ...RemoteFlags, @@ -28,18 +29,30 @@ export class CreateSignatureShareCommand extends IronfishCommand { default: false, description: 'Confirm creating signature share without confirming', }), + path: Flags.string({ + description: 'Path to a JSON file containing multisig transaction data', + }), } async start(): Promise { const { flags } = await this.parse(CreateSignatureShareCommand) - let signingPackage = flags.signingPackage?.trim() - if (!signingPackage) { - signingPackage = await longPrompt('Enter the signing package') + const loaded = await MultisigTransactionJson.load(this.sdk.fileSystem, flags.path) + const options = MultisigTransactionJson.resolveFlags(flags, loaded) + + let signingPackageString = options.signingPackage + if (!signingPackageString) { + signingPackageString = await longPrompt('Enter the signing package') } const client = await this.sdk.connectRpc() - const unsignedTransaction = UnsignedTransaction.fromSigningPackage(signingPackage) + + const signingPackage = new multisig.SigningPackage(Buffer.from(signingPackageString, 'hex')) + const unsignedTransaction = new UnsignedTransaction( + signingPackage.unsignedTransaction().serialize(), + ) + + this.renderSigners(signingPackage.signers()) await renderUnsignedTransactionDetails( client, @@ -57,11 +70,31 @@ export class CreateSignatureShareCommand extends IronfishCommand { const signatureShareResponse = await client.wallet.multisig.createSignatureShare({ account: flags.account, - signingPackage, + signingPackage: signingPackageString, }) this.log() this.log('Signature Share:') this.log(signatureShareResponse.content.signatureShare) + + this.log() + this.log('Next step:') + this.log( + 'Send the signature to the coordinator. They will aggregate the signatures from all participants and sign the transaction.', + ) + } + + renderSigners(signers: Buffer[]): void { + this.log('') + this.log('==================') + this.log('Signer Identities:') + this.log('==================') + + for (const [i, signer] of signers.entries()) { + if (i !== 0) { + this.log('------------------') + } + this.log(signer.toString('hex')) + } } } diff --git a/ironfish-cli/src/utils/multisig.ts b/ironfish-cli/src/utils/multisig.ts new file mode 100644 index 0000000000..f41367eefc --- /dev/null +++ b/ironfish-cli/src/utils/multisig.ts @@ -0,0 +1,70 @@ +/* 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 { FileSystem, YupUtils } from '@ironfish/sdk' +import * as yup from 'yup' + +export type MultisigTransactionOptions = { + identity: string[] + unsignedTransaction: string + commitment: string[] + signingPackage: string + signatureShare: string[] +} + +export const MultisigTransactionOptionsSchema: yup.ObjectSchema< + Partial +> = yup + .object({ + identity: yup.array().of(yup.string().defined()), + unsignedTransaction: yup.string(), + commitment: yup.array().of(yup.string().defined()), + signingPackage: yup.string(), + signatureShare: yup.array().of(yup.string().defined()), + }) + .defined() + +async function load( + files: FileSystem, + path?: string, +): Promise> { + if (path === undefined) { + return {} + } + + const data = (await files.readFile(files.resolve(path))).trim() + + const { error, result } = await YupUtils.tryValidate(MultisigTransactionOptionsSchema, data) + + if (error) { + throw error + } + + return result +} + +type MultisigTransactionFlags = { + identity?: string[] + unsignedTransaction?: string + commitment?: string[] + signingPackage?: string + signatureShare?: string[] +} + +function resolveFlags( + flags: MultisigTransactionFlags, + json: Partial, +): Partial { + return { + identity: flags.identity ?? json.identity, + unsignedTransaction: flags.unsignedTransaction?.trim() ?? json.unsignedTransaction, + commitment: flags.commitment ?? json.commitment, + signingPackage: flags.signingPackage?.trim() ?? json.signingPackage, + signatureShare: flags.signatureShare ?? json.signatureShare, + } +} + +export const MultisigTransactionJson = { + load, + resolveFlags, +} diff --git a/ironfish-cli/src/utils/transaction.ts b/ironfish-cli/src/utils/transaction.ts index 314286d319..bcdf5be195 100644 --- a/ironfish-cli/src/utils/transaction.ts +++ b/ironfish-cli/src/utils/transaction.ts @@ -74,51 +74,52 @@ export async function renderUnsignedTransactionDetails( unsignedTransaction: unsignedTransaction.serialize().toString('hex'), }) - if (response.content.sentNotes.length > 0) { + logger.log('') + logger.log('==================') + logger.log('Notes sent:') + logger.log('==================') + + for (const [i, note] of response.content.sentNotes.entries()) { + // Skip logger since we'll re-render for received notes + if (note.owner === note.sender) { + continue + } + + if (i !== 0) { + logger.log('------------------') + } logger.log('') - logger.log('==================') - logger.log('Notes sent:') - logger.log('==================') - - let logged = false - for (const note of response.content.sentNotes) { - // Skip logger since we'll re-render for received notes - if (note.owner === note.sender) { - continue - } - - if (logged) { - logger.log('------------------') - } - logged = true - logger.log('') - - logger.log(`Amount: ${CurrencyUtils.renderIron(note.value, true, note.assetId)}`) - logger.log(`Memo: ${note.memo}`) - logger.log(`Recipient: ${note.owner}`) - logger.log(`Sender: ${note.sender}`) - logger.log('') + + logger.log(`Amount: ${CurrencyUtils.renderIron(note.value, true, note.assetId)}`) + logger.log(`Memo: ${note.memo}`) + logger.log(`Recipient: ${note.owner}`) + logger.log(`Sender: ${note.sender}`) + logger.log('') + } + + logger.log('') + logger.log('==================') + logger.log('Notes received:') + logger.log('==================') + + for (const [i, note] of response.content.receivedNotes.entries()) { + if (i !== 0) { + logger.log('------------------') } + logger.log('') + + logger.log(`Amount: ${CurrencyUtils.renderIron(note.value, true, note.assetId)}`) + logger.log(`Memo: ${note.memo}`) + logger.log(`Recipient: ${note.owner}`) + logger.log(`Sender: ${note.sender}`) + logger.log('') } - if (response.content.receivedNotes.length > 0) { + if (!response.content.sentNotes.length && !response.content.receivedNotes.length) { logger.log('') - logger.log('==================') - logger.log('Notes received:') - logger.log('==================') - - for (const [i, note] of response.content.receivedNotes.entries()) { - if (i !== 0) { - logger.log('------------------') - } - logger.log('') - - logger.log(`Amount: ${CurrencyUtils.renderIron(note.value, true, note.assetId)}`) - logger.log(`Memo: ${note.memo}`) - logger.log(`Recipient: ${note.owner}`) - logger.log(`Sender: ${note.sender}`) - logger.log('') - } + logger.log('------------------') + logger.log('Account unable to decrypt any notes in this transaction') + logger.log('------------------') } } diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index eeafbc0b4c..3db03c85f8 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -3,24 +3,6 @@ /* auto-generated by NAPI-RS */ -export const IDENTITY_LEN: number -export const SECRET_LEN: number -export function createSigningCommitment(secret: string, keyPackage: string, transactionHash: Buffer, signers: Array): string -export function createSignatureShare(secret: string, keyPackage: string, signingPackage: string): string -export function generateAndSplitKey(minSigners: number, identities: Array): TrustedDealerKeyPackages -export interface ParticipantKeyPackage { - identity: string - keyPackage: string -} -export interface TrustedDealerKeyPackages { - publicAddress: string - publicKeyPackage: string - viewKey: string - incomingViewKey: string - outgoingViewKey: string - proofAuthorizingKey: string - keyPackages: Array -} export function contribute(inputPath: string, outputPath: string, seed?: string | undefined | null): Promise export function verifyTransform(paramsPath: string, newParamsPath: string): Promise export const KEY_LENGTH: number @@ -67,7 +49,6 @@ export const TRANSACTION_EXPIRATION_LENGTH: number export const TRANSACTION_FEE_LENGTH: number export const LATEST_TRANSACTION_VERSION: number export function verifyTransactions(serializedTransactions: Array): boolean -export function aggregateSignatureShares(publicKeyPackageStr: string, signingPackageStr: string, signatureSharesArr: Array): Buffer export const enum LanguageCode { English = 0, ChineseSimplified = 1, @@ -97,28 +78,6 @@ export class FishHashContext { prebuildDataset(threads: number): void hash(header: Buffer): Buffer } -export class ParticipantSecret { - constructor(jsBytes: Buffer) - serialize(): Buffer - static random(): ParticipantSecret - toIdentity(): ParticipantIdentity - decryptData(jsBytes: Buffer): Buffer -} -export class ParticipantIdentity { - constructor(jsBytes: Buffer) - serialize(): Buffer - encryptData(jsBytes: Buffer): Buffer -} -export type NativePublicKeyPackage = PublicKeyPackage -export class PublicKeyPackage { - constructor(value: string) - identities(): Array -} -export type NativeSigningCommitment = SigningCommitment -export class SigningCommitment { - constructor(jsBytes: Buffer) - identity(): Buffer -} export class BoxKeyPair { constructor() static fromHex(secretHex: string): BoxKeyPair @@ -253,7 +212,6 @@ export class Transaction { export type NativeUnsignedTransaction = UnsignedTransaction export class UnsignedTransaction { constructor(jsBytes: Buffer) - static fromSigningPackage(signingPackageStr: string): NativeUnsignedTransaction serialize(): Buffer publicKeyRandomness(): string hash(): Buffer @@ -273,3 +231,54 @@ export class ThreadPoolHandler { getFoundBlock(): FoundBlockResult | null getHashRateSubmission(): number } +export namespace multisig { + export const IDENTITY_LEN: number + export const SECRET_LEN: number + export function createSigningCommitment(secret: string, keyPackage: string, transactionHash: Buffer, signers: Array): string + export function createSignatureShare(secret: string, keyPackage: string, signingPackage: string): string + export function generateAndSplitKey(minSigners: number, identities: Array): TrustedDealerKeyPackages + export interface ParticipantKeyPackage { + identity: string + keyPackage: string + } + export interface TrustedDealerKeyPackages { + publicAddress: string + publicKeyPackage: string + viewKey: string + incomingViewKey: string + outgoingViewKey: string + proofAuthorizingKey: string + keyPackages: Array + } + export function aggregateSignatureShares(publicKeyPackageStr: string, signingPackageStr: string, signatureSharesArr: Array): Buffer + export class ParticipantSecret { + constructor(jsBytes: Buffer) + serialize(): Buffer + static random(): ParticipantSecret + toIdentity(): ParticipantIdentity + decryptData(jsBytes: Buffer): Buffer + } + export class ParticipantIdentity { + constructor(jsBytes: Buffer) + serialize(): Buffer + encryptData(jsBytes: Buffer): Buffer + } + export type NativePublicKeyPackage = PublicKeyPackage + export class PublicKeyPackage { + constructor(value: string) + identities(): Array + minSigners(): number + } + export type NativeSigningCommitment = SigningCommitment + export class SigningCommitment { + constructor(jsBytes: Buffer) + identity(): Buffer + verifyChecksum(transactionHash: Buffer, signerIdentities: Array): boolean + } + export type NativeSigningPackage = SigningPackage + export class SigningPackage { + constructor(jsBytes: Buffer) + unsignedTransaction(): NativeUnsignedTransaction + signers(): Array + } +} diff --git a/ironfish-rust-nodejs/index.js b/ironfish-rust-nodejs/index.js index 865f8b861d..a858c6522d 100644 --- a/ironfish-rust-nodejs/index.js +++ b/ironfish-rust-nodejs/index.js @@ -252,18 +252,9 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { FishHashContext, IDENTITY_LEN, SECRET_LEN, createSigningCommitment, createSignatureShare, ParticipantSecret, ParticipantIdentity, generateAndSplitKey, PublicKeyPackage, SigningCommitment, contribute, verifyTransform, KEY_LENGTH, NONCE_LENGTH, BoxKeyPair, randomBytes, boxMessage, unboxMessage, RollingFilter, initSignalHandler, triggerSegfault, ASSET_ID_LENGTH, ASSET_METADATA_LENGTH, ASSET_NAME_LENGTH, ASSET_LENGTH, Asset, NOTE_ENCRYPTION_KEY_LENGTH, MAC_LENGTH, ENCRYPTED_NOTE_PLAINTEXT_LENGTH, ENCRYPTED_NOTE_LENGTH, NoteEncrypted, PUBLIC_ADDRESS_LENGTH, RANDOMNESS_LENGTH, MEMO_LENGTH, AMOUNT_VALUE_LENGTH, DECRYPTED_NOTE_LENGTH, Note, PROOF_LENGTH, TRANSACTION_SIGNATURE_LENGTH, TRANSACTION_PUBLIC_KEY_RANDOMNESS_LENGTH, TRANSACTION_EXPIRATION_LENGTH, TRANSACTION_FEE_LENGTH, LATEST_TRANSACTION_VERSION, TransactionPosted, Transaction, verifyTransactions, UnsignedTransaction, aggregateSignatureShares, LanguageCode, generateKey, spendingKeyToWords, wordsToSpendingKey, generateKeyFromPrivateKey, initializeSapling, FoundBlockResult, ThreadPoolHandler, isValidPublicAddress } = nativeBinding +const { FishHashContext, contribute, verifyTransform, KEY_LENGTH, NONCE_LENGTH, BoxKeyPair, randomBytes, boxMessage, unboxMessage, RollingFilter, initSignalHandler, triggerSegfault, ASSET_ID_LENGTH, ASSET_METADATA_LENGTH, ASSET_NAME_LENGTH, ASSET_LENGTH, Asset, NOTE_ENCRYPTION_KEY_LENGTH, MAC_LENGTH, ENCRYPTED_NOTE_PLAINTEXT_LENGTH, ENCRYPTED_NOTE_LENGTH, NoteEncrypted, PUBLIC_ADDRESS_LENGTH, RANDOMNESS_LENGTH, MEMO_LENGTH, AMOUNT_VALUE_LENGTH, DECRYPTED_NOTE_LENGTH, Note, PROOF_LENGTH, TRANSACTION_SIGNATURE_LENGTH, TRANSACTION_PUBLIC_KEY_RANDOMNESS_LENGTH, TRANSACTION_EXPIRATION_LENGTH, TRANSACTION_FEE_LENGTH, LATEST_TRANSACTION_VERSION, TransactionPosted, Transaction, verifyTransactions, UnsignedTransaction, LanguageCode, generateKey, spendingKeyToWords, wordsToSpendingKey, generateKeyFromPrivateKey, initializeSapling, FoundBlockResult, ThreadPoolHandler, isValidPublicAddress, multisig } = nativeBinding module.exports.FishHashContext = FishHashContext -module.exports.IDENTITY_LEN = IDENTITY_LEN -module.exports.SECRET_LEN = SECRET_LEN -module.exports.createSigningCommitment = createSigningCommitment -module.exports.createSignatureShare = createSignatureShare -module.exports.ParticipantSecret = ParticipantSecret -module.exports.ParticipantIdentity = ParticipantIdentity -module.exports.generateAndSplitKey = generateAndSplitKey -module.exports.PublicKeyPackage = PublicKeyPackage -module.exports.SigningCommitment = SigningCommitment module.exports.contribute = contribute module.exports.verifyTransform = verifyTransform module.exports.KEY_LENGTH = KEY_LENGTH @@ -301,7 +292,6 @@ module.exports.TransactionPosted = TransactionPosted module.exports.Transaction = Transaction module.exports.verifyTransactions = verifyTransactions module.exports.UnsignedTransaction = UnsignedTransaction -module.exports.aggregateSignatureShares = aggregateSignatureShares module.exports.LanguageCode = LanguageCode module.exports.generateKey = generateKey module.exports.spendingKeyToWords = spendingKeyToWords @@ -311,3 +301,4 @@ module.exports.initializeSapling = initializeSapling module.exports.FoundBlockResult = FoundBlockResult module.exports.ThreadPoolHandler = ThreadPoolHandler module.exports.isValidPublicAddress = isValidPublicAddress +module.exports.multisig = multisig diff --git a/ironfish-rust-nodejs/npm/darwin-arm64/package.json b/ironfish-rust-nodejs/npm/darwin-arm64/package.json index c03a1d7307..73e9c591d2 100644 --- a/ironfish-rust-nodejs/npm/darwin-arm64/package.json +++ b/ironfish-rust-nodejs/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-darwin-arm64", - "version": "2.0.0", + "version": "2.1.0", "os": [ "darwin" ], diff --git a/ironfish-rust-nodejs/npm/darwin-x64/package.json b/ironfish-rust-nodejs/npm/darwin-x64/package.json index faa7b3e0e2..29a45326cb 100644 --- a/ironfish-rust-nodejs/npm/darwin-x64/package.json +++ b/ironfish-rust-nodejs/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-darwin-x64", - "version": "2.0.0", + "version": "2.1.0", "os": [ "darwin" ], diff --git a/ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json b/ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json index c81e40ab90..56553f02df 100644 --- a/ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json +++ b/ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-linux-arm64-gnu", - "version": "2.0.0", + "version": "2.1.0", "os": [ "linux" ], diff --git a/ironfish-rust-nodejs/npm/linux-arm64-musl/package.json b/ironfish-rust-nodejs/npm/linux-arm64-musl/package.json index 6ec1ff2135..8db3e82026 100644 --- a/ironfish-rust-nodejs/npm/linux-arm64-musl/package.json +++ b/ironfish-rust-nodejs/npm/linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-linux-arm64-musl", - "version": "2.0.0", + "version": "2.1.0", "os": [ "linux" ], diff --git a/ironfish-rust-nodejs/npm/linux-x64-gnu/package.json b/ironfish-rust-nodejs/npm/linux-x64-gnu/package.json index 75c40f91ad..556464255d 100644 --- a/ironfish-rust-nodejs/npm/linux-x64-gnu/package.json +++ b/ironfish-rust-nodejs/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-linux-x64-gnu", - "version": "2.0.0", + "version": "2.1.0", "os": [ "linux" ], diff --git a/ironfish-rust-nodejs/npm/linux-x64-musl/package.json b/ironfish-rust-nodejs/npm/linux-x64-musl/package.json index 5e13c9d289..14cdb21165 100644 --- a/ironfish-rust-nodejs/npm/linux-x64-musl/package.json +++ b/ironfish-rust-nodejs/npm/linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-linux-x64-musl", - "version": "2.0.0", + "version": "2.1.0", "os": [ "linux" ], diff --git a/ironfish-rust-nodejs/npm/win32-x64-msvc/package.json b/ironfish-rust-nodejs/npm/win32-x64-msvc/package.json index ac55f91735..4409c3ef89 100644 --- a/ironfish-rust-nodejs/npm/win32-x64-msvc/package.json +++ b/ironfish-rust-nodejs/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-win32-x64-msvc", - "version": "2.0.0", + "version": "2.1.0", "os": [ "win32" ], diff --git a/ironfish-rust-nodejs/package.json b/ironfish-rust-nodejs/package.json index e62db86a4b..e991ec1dbc 100644 --- a/ironfish-rust-nodejs/package.json +++ b/ironfish-rust-nodejs/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs", - "version": "2.0.0", + "version": "2.1.0", "description": "Node.js bindings for Rust code required by the Iron Fish SDK", "main": "index.js", "types": "index.d.ts", diff --git a/ironfish-rust-nodejs/src/lib.rs b/ironfish-rust-nodejs/src/lib.rs index bebeeb688a..ca77a59134 100644 --- a/ironfish-rust-nodejs/src/lib.rs +++ b/ironfish-rust-nodejs/src/lib.rs @@ -16,8 +16,8 @@ use ironfish::mining; use ironfish::sapling_bls12; pub mod fish_hash; -pub mod frost; pub mod mpc; +pub mod multisig; pub mod nacl; pub mod rolling_filter; pub mod signal_catcher; diff --git a/ironfish-rust-nodejs/src/frost.rs b/ironfish-rust-nodejs/src/multisig.rs similarity index 75% rename from ironfish-rust-nodejs/src/frost.rs rename to ironfish-rust-nodejs/src/multisig.rs index 08a7799e03..50305c92e6 100644 --- a/ironfish-rust-nodejs/src/frost.rs +++ b/ironfish-rust-nodejs/src/multisig.rs @@ -2,9 +2,9 @@ * 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/. */ -use crate::to_napi_err; +use crate::{structs::NativeUnsignedTransaction, to_napi_err}; use ironfish::{ - frost::{keys::KeyPackage, round1::SigningCommitments, round2, Randomizer}, + frost::{keys::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}, @@ -19,10 +19,10 @@ use napi_derive::napi; use rand::thread_rng; use std::ops::Deref; -#[napi] +#[napi(namespace = "multisig")] pub const IDENTITY_LEN: u32 = ironfish::frost_utils::IDENTITY_LEN as u32; -#[napi] +#[napi(namespace = "multisig")] pub const SECRET_LEN: u32 = ironfish_frost::participant::SECRET_LEN as u32; fn try_deserialize_identities(signers: I) -> Result> @@ -44,7 +44,7 @@ where }) } -#[napi] +#[napi(namespace = "multisig")] pub fn create_signing_commitment( secret: String, key_package: String, @@ -58,19 +58,18 @@ pub fn create_signing_commitment( let transaction_hash = transaction_hash.into_value()?; let signers = try_deserialize_identities(signers)?; - let nonces = - deterministic_signing_nonces(key_package.signing_share(), &transaction_hash, &signers); - let commitments = SigningCommitments::from(&nonces); - - let signing_commitment = - SigningCommitment::from_frost(secret, *commitments.hiding(), *commitments.binding()); - - let bytes = signing_commitment.serialize()?; + let signing_commitment = SigningCommitment::from_secrets( + &secret, + key_package.signing_share(), + &transaction_hash, + &signers, + ); + let bytes = signing_commitment.serialize(); Ok(bytes_to_hex(&bytes[..])) } -#[napi] +#[napi(namespace = "multisig")] pub fn create_signature_share( secret: String, key_package: String, @@ -119,12 +118,12 @@ pub fn create_signature_share( Ok(bytes_to_hex(&bytes[..])) } -#[napi] +#[napi(namespace = "multisig")] pub struct ParticipantSecret { secret: Secret, } -#[napi] +#[napi(namespace = "multisig")] impl ParticipantSecret { #[napi(constructor)] pub fn new(js_bytes: JsBuffer) -> Result { @@ -162,12 +161,12 @@ impl ParticipantSecret { } } -#[napi] +#[napi(namespace = "multisig")] pub struct ParticipantIdentity { identity: Identity, } -#[napi] +#[napi(namespace = "multisig")] impl ParticipantIdentity { #[napi(constructor)] pub fn new(js_bytes: JsBuffer) -> Result { @@ -193,7 +192,7 @@ impl ParticipantIdentity { } } -#[napi] +#[napi(namespace = "multisig")] pub fn generate_and_split_key( min_signers: u16, identities: Vec, @@ -203,13 +202,22 @@ pub fn generate_and_split_key( let identities = try_deserialize_identities(identities)?; let packages = - split_spender_key(&spending_key, min_signers, identities).map_err(to_napi_err)?; + split_spender_key(&spending_key, min_signers, &identities).map_err(to_napi_err)?; let mut key_packages = Vec::with_capacity(packages.key_packages.len()); - for (identity, key_package) in packages.key_packages.iter() { + + // preserves the order of the identities + for identity in identities { + let key_package = packages + .key_packages + .get(&identity) + .ok_or_else(|| to_napi_err("Key package not found for identity"))? + .serialize() + .map_err(to_napi_err)?; + key_packages.push(ParticipantKeyPackage { identity: bytes_to_hex(&identity.serialize()), - key_package: bytes_to_hex(&key_package.serialize().map_err(to_napi_err)?), + key_package: bytes_to_hex(&key_package), }); } @@ -229,7 +237,7 @@ pub fn generate_and_split_key( }) } -#[napi(object)] +#[napi(object, namespace = "multisig")] pub struct ParticipantKeyPackage { pub identity: String, // TODO: this should contain the spender_key only, there's no need to return (and later store) @@ -239,7 +247,7 @@ pub struct ParticipantKeyPackage { pub key_package: String, } -#[napi(object)] +#[napi(object, namespace = "multisig")] pub struct TrustedDealerKeyPackages { pub public_address: String, pub public_key_package: String, @@ -250,12 +258,12 @@ pub struct TrustedDealerKeyPackages { pub key_packages: Vec, } -#[napi(js_name = "PublicKeyPackage")] +#[napi(js_name = "PublicKeyPackage", namespace = "multisig")] pub struct NativePublicKeyPackage { public_key_package: PublicKeyPackage, } -#[napi] +#[napi(namespace = "multisig")] impl NativePublicKeyPackage { #[napi(constructor)] pub fn new(value: String) -> Result { @@ -275,14 +283,19 @@ impl NativePublicKeyPackage { .map(|identity| Buffer::from(&identity.serialize()[..])) .collect() } + + #[napi] + pub fn min_signers(&self) -> u16 { + self.public_key_package.min_signers() + } } -#[napi(js_name = "SigningCommitment")] +#[napi(js_name = "SigningCommitment", namespace = "multisig")] pub struct NativeSigningCommitment { signing_commitment: SigningCommitment, } -#[napi] +#[napi(namespace = "multisig")] impl NativeSigningCommitment { #[napi(constructor)] pub fn new(js_bytes: JsBuffer) -> Result { @@ -296,4 +309,50 @@ impl NativeSigningCommitment { pub fn identity(&self) -> Buffer { Buffer::from(self.signing_commitment.identity().serialize().as_slice()) } + + #[napi] + pub fn verify_checksum( + &self, + transaction_hash: JsBuffer, + signer_identities: Vec, + ) -> Result { + let transaction_hash = transaction_hash.into_value()?; + let signer_identities = try_deserialize_identities(signer_identities)?; + Ok(self + .signing_commitment + .verify_checksum(&transaction_hash, &signer_identities) + .is_ok()) + } +} + +#[napi(js_name = "SigningPackage", namespace = "multisig")] +pub struct NativeSigningPackage { + signing_package: SigningPackage, +} + +#[napi(namespace = "multisig")] +impl NativeSigningPackage { + #[napi(constructor)] + pub fn new(js_bytes: JsBuffer) -> Result { + let bytes = js_bytes.into_value()?; + SigningPackage::read(bytes.as_ref()) + .map(|signing_package| NativeSigningPackage { signing_package }) + .map_err(to_napi_err) + } + + #[napi] + pub fn unsigned_transaction(&self) -> NativeUnsignedTransaction { + NativeUnsignedTransaction { + transaction: self.signing_package.unsigned_transaction.clone(), + } + } + + #[napi] + pub fn signers(&self) -> Vec { + self.signing_package + .signers + .iter() + .map(|signer| Buffer::from(&signer.serialize()[..])) + .collect() + } } diff --git a/ironfish-rust-nodejs/src/structs/transaction.rs b/ironfish-rust-nodejs/src/structs/transaction.rs index d14dd55b1b..0f381aad1a 100644 --- a/ironfish-rust-nodejs/src/structs/transaction.rs +++ b/ironfish-rust-nodejs/src/structs/transaction.rs @@ -383,7 +383,7 @@ pub fn verify_transactions(serialized_transactions: Vec) -> Result Result { - let bytes = hex_to_vec_bytes(&signing_package_str).map_err(to_napi_err)?; - let signing_package = SigningPackage::read(&bytes[..]).map_err(to_napi_err)?; - - Ok(NativeUnsignedTransaction { - transaction: signing_package.unsigned_transaction, - }) - } - #[napi] pub fn serialize(&self) -> Result { let mut vec: Vec = vec![]; @@ -472,7 +462,7 @@ impl NativeUnsignedTransaction { } } -#[napi] +#[napi(namespace = "multisig")] pub fn aggregate_signature_shares( public_key_package_str: String, signing_package_str: String, diff --git a/ironfish-rust-nodejs/tests/frost.test.slow.ts b/ironfish-rust-nodejs/tests/frost.test.slow.ts index ab45dc4dd7..daa08b39c8 100644 --- a/ironfish-rust-nodejs/tests/frost.test.slow.ts +++ b/ironfish-rust-nodejs/tests/frost.test.slow.ts @@ -2,18 +2,18 @@ * 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 { ParticipantIdentity, ParticipantSecret } from ".."; +import { multisig } from ".."; describe('ParticipantIdentity', () => { describe('ser/de', () => { it('serializes and deserializes as a buffer', () => { - const secret = ParticipantSecret.random() + const secret = multisig.ParticipantSecret.random() const identity = secret.toIdentity() const serialized = identity.serialize() - const deserialized = new ParticipantIdentity(serialized) + const deserialized = new multisig.ParticipantIdentity(serialized) expect(identity).toEqual(deserialized) }) diff --git a/ironfish-rust/src/errors.rs b/ironfish-rust/src/errors.rs index 23836e4f83..446e1ea077 100644 --- a/ironfish-rust/src/errors.rs +++ b/ironfish-rust/src/errors.rs @@ -40,8 +40,6 @@ pub enum IronfishErrorKind { InvalidDiversificationPoint, InvalidEntropy, InvalidFr, - InvalidFrostIdentifier, - InvalidFrostSignatureShare, InvalidLanguageEncoding, InvalidMinersFeeTransaction, InvalidMintProof, diff --git a/ironfish-rust/src/frost_utils/split_secret.rs b/ironfish-rust/src/frost_utils/split_secret.rs index 006c2738e1..6112c50740 100644 --- a/ironfish-rust/src/frost_utils/split_secret.rs +++ b/ironfish-rust/src/frost_utils/split_secret.rs @@ -12,53 +12,84 @@ use ironfish_frost::{ keys::PublicKeyPackage, }; use rand::{CryptoRng, RngCore}; -use std::collections::HashMap; +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + hash::Hash, +}; -use crate::errors::IronfishError; +use crate::errors::{IronfishError, IronfishErrorKind}; use crate::SaplingKey; -pub struct SecretShareConfig { - pub min_signers: u16, - pub identities: Vec, - pub spender_key: SaplingKey, +/// Checks a sequence for duplicates; if any, the first duplicate found is returned as an error, +/// else the whole sequence is returned as a `HashSet` +fn find_dupes(it: I) -> Result, I::Item> +where + I: IntoIterator, + I::Item: Eq + Hash, + I::IntoIter: ExactSizeIterator, +{ + let it = it.into_iter(); + let mut set = HashSet::with_capacity(it.len()); + for item in it { + if let Some(dupe) = set.replace(item) { + return Err(dupe); + } + } + Ok(set) } pub(crate) fn split_secret( - config: &SecretShareConfig, + spender_key: &SaplingKey, + identities: &[Identity], + min_signers: u16, mut rng: R, ) -> Result<(HashMap, PublicKeyPackage), IronfishError> { - let mut frost_id_map = config - .identities + // Catch duplicate identities. We could in theory just remove duplicates, but doing so might + // give users the impression that the maximum number of signers is `identities.len()`, while + // it's actually lower than that. + let identities = find_dupes(identities.iter().cloned()).map_err(|dupe| { + IronfishError::new_with_source( + IronfishErrorKind::InvalidData, + format!("duplicate identity: {:?}", dupe), + ) + })?; + let num_identities = identities.len(); + + let mut frost_id_map = identities .iter() .cloned() .map(|identity| (identity.to_frost_identifier(), identity)) - .collect::>(); + .collect::>(); + assert_eq!( + num_identities, + frost_id_map.len(), + "frost identitifer collision" + ); + let frost_ids = frost_id_map.keys().cloned().collect::>(); let identifier_list = IdentifierList::Custom(&frost_ids[..]); - let secret_key = SigningKey::deserialize(config.spender_key.spend_authorizing_key.to_bytes())?; - let max_signers: u16 = config.identities.len().try_into()?; + let secret_key = SigningKey::deserialize(spender_key.spend_authorizing_key.to_bytes())?; + let max_signers: u16 = num_identities.try_into()?; let (shares, pubkeys) = split( &secret_key, max_signers, - config.min_signers, + min_signers, identifier_list, &mut rng, )?; - let mut key_packages: HashMap<_, _> = HashMap::new(); - + let mut key_packages = HashMap::new(); for (frost_id, secret_share) in shares { let identity = frost_id_map .remove(&frost_id) .expect("frost returned an identifier that was not passed as an input"); - let key_package = KeyPackage::try_from(secret_share.clone())?; + let key_package = KeyPackage::try_from(secret_share)?; key_packages.insert(identity, key_package); } - let public_key_package = - PublicKeyPackage::from_frost(pubkeys, config.identities.iter().cloned()); + let public_key_package = PublicKeyPackage::from_frost(pubkeys, identities, min_signers); Ok((key_packages, public_key_package)) } @@ -68,22 +99,16 @@ mod test { use super::*; use crate::{keys::SaplingKey, test_util::create_multisig_identities}; use ironfish_frost::frost::{frost::keys::reconstruct, JubjubBlake2b512}; + use rand::thread_rng; #[test] fn test_split_secret() { let identities = create_multisig_identities(10); let identities_length = identities.len(); - let rng = rand::thread_rng(); let key = SaplingKey::generate_key(); - let config = SecretShareConfig { - min_signers: 2, - identities, - spender_key: key, - }; - - let (key_packages, _) = split_secret(&config, rng).unwrap(); + let (key_packages, _) = split_secret(&key, &identities, 2, thread_rng()).unwrap(); assert_eq!(key_packages.len(), identities_length); let key_parts: Vec<_> = key_packages.values().cloned().collect(); @@ -93,9 +118,6 @@ mod test { let scalar = signing_key.to_scalar(); - assert_eq!( - scalar.to_bytes(), - config.spender_key.spend_authorizing_key.to_bytes() - ); + assert_eq!(scalar.to_bytes(), key.spend_authorizing_key.to_bytes()); } } diff --git a/ironfish-rust/src/frost_utils/split_spender_key.rs b/ironfish-rust/src/frost_utils/split_spender_key.rs index 26dce4c8b9..3eab778c5e 100644 --- a/ironfish-rust/src/frost_utils/split_spender_key.rs +++ b/ironfish-rust/src/frost_utils/split_spender_key.rs @@ -15,7 +15,7 @@ use crate::{ IncomingViewKey, OutgoingViewKey, PublicAddress, SaplingKey, ViewKey, }; -use super::split_secret::{split_secret, SecretShareConfig}; +use super::split_secret::split_secret; pub struct TrustedDealerKeyPackages { pub public_address: PublicAddress, @@ -30,19 +30,14 @@ pub struct TrustedDealerKeyPackages { pub fn split_spender_key( spender_key: &SaplingKey, min_signers: u16, - identities: Vec, + identities: &[Identity], ) -> Result { let group_secret_key = SaplingKey::generate_key(); - let secret_config = SecretShareConfig { - min_signers, - identities, - spender_key: spender_key.clone(), - }; - - let (key_packages, public_key_package) = split_secret(&secret_config, thread_rng())?; + let (key_packages, public_key_package) = + split_secret(spender_key, identities, min_signers, thread_rng())?; - let proof_authorizing_key = secret_config.spender_key.sapling_proof_generation_key().nsk; + let proof_authorizing_key = spender_key.sapling_proof_generation_key().nsk; let authorizing_key = public_key_package.verifying_key().serialize(); let authorizing_key = Option::from(SubgroupPoint::from_bytes(&authorizing_key)) @@ -55,7 +50,7 @@ pub fn split_spender_key( nullifier_deriving_key, }; - let incoming_view_key = secret_config.spender_key.incoming_view_key().clone(); + let incoming_view_key = spender_key.incoming_view_key().clone(); let outgoing_view_key: OutgoingViewKey = group_secret_key.outgoing_view_key().clone(); let public_address = incoming_view_key.public_address(); @@ -88,7 +83,7 @@ mod test { let sapling_key = SaplingKey::generate_key(); let trusted_dealer_key_packages = - split_spender_key(&sapling_key, 5, identities).expect("spender key split failed"); + split_spender_key(&sapling_key, 5, &identities).expect("spender key split failed"); assert_eq!( trusted_dealer_key_packages.key_packages.len(), diff --git a/ironfish-rust/src/keys/mod.rs b/ironfish-rust/src/keys/mod.rs index f621686398..5797f6f1fd 100644 --- a/ironfish-rust/src/keys/mod.rs +++ b/ironfish-rust/src/keys/mod.rs @@ -13,7 +13,6 @@ use group::GroupEncoding; use ironfish_zkp::constants::{ CRH_IVK_PERSONALIZATION, PROOF_GENERATION_KEY_GENERATOR, SPENDING_KEY_GENERATOR, }; -use ironfish_zkp::ProofGenerationKey; use jubjub::SubgroupPoint; use rand::prelude::*; diff --git a/ironfish-rust/src/serializing/mod.rs b/ironfish-rust/src/serializing/mod.rs index 8e8752edce..1555eaf690 100644 --- a/ironfish-rust/src/serializing/mod.rs +++ b/ironfish-rust/src/serializing/mod.rs @@ -72,7 +72,7 @@ pub fn hex_to_vec_bytes(hex: &str) -> Result, IronfishError> { let hex_iter = hex.as_bytes().chunks_exact(2); - for (_, hex) in hex_iter.enumerate() { + for hex in hex_iter { bytes.push(hex_to_u8(hex[0])? << 4 | hex_to_u8(hex[1])?); } diff --git a/ironfish-rust/src/test_util.rs b/ironfish-rust/src/test_util.rs index b015c8c7dd..5e80ebf9f3 100644 --- a/ironfish-rust/src/test_util.rs +++ b/ironfish-rust/src/test_util.rs @@ -63,7 +63,6 @@ pub(crate) fn auth_path_to_root_hash( /// Helper function to create a list of random identifiers for multisig participants. pub fn create_multisig_identities(num_identifiers: usize) -> Vec { (0..num_identifiers) - .into_iter() .map(|_| Secret::random(thread_rng()).to_identity()) .collect() } diff --git a/ironfish-rust/src/transaction/tests.rs b/ironfish-rust/src/transaction/tests.rs index c35fec7748..5e24aef5a1 100644 --- a/ironfish-rust/src/transaction/tests.rs +++ b/ironfish-rust/src/transaction/tests.rs @@ -385,7 +385,6 @@ fn test_transaction_version_is_checked() { let valid_versions = [1u8, 2u8]; let invalid_versions = (u8::MIN..=u8::MAX) - .into_iter() .filter(|v| !valid_versions.contains(v)) .collect::>(); assert_eq!(invalid_versions.len(), 254); @@ -708,7 +707,7 @@ fn test_aggregate_signature_shares() { let identities = create_multisig_identities(10); // key package generation by trusted dealer - let key_packages = split_spender_key(&spender_key, 2, identities.clone()) + let key_packages = split_spender_key(&spender_key, 2, &identities) .expect("should be able to split spender key"); // create raw/proposed transaction diff --git a/ironfish-zkp/src/circuits/spend.rs b/ironfish-zkp/src/circuits/spend.rs index 0b6d658177..7546711dad 100644 --- a/ironfish-zkp/src/circuits/spend.rs +++ b/ironfish-zkp/src/circuits/spend.rs @@ -501,7 +501,7 @@ mod test { let tree_depth = 32; - let expected_commitment_us = vec![ + let expected_commitment_us = [ "43821661663052659750276289184181083197337192946256245809816728673021647664276", "3307162152126086816128645612622677680790809218093944138874328908293932504970", "11069610839860164669107523771435662310747439806735198902948128278779049984187", @@ -509,7 +509,7 @@ mod test { "21486204115269202361511785053830008932611482941164104173096094940232117952518", ]; - let expected_commitment_vs = vec![ + let expected_commitment_vs = [ "27630722367128086497290371604583225252915685718989450292520883698391703910", "50661753032157861157033186529508424590095922868336394465083465551064239171765", "42720677731824278375166374390978871535259136440715287500660141460976255671332", diff --git a/ironfish/package.json b/ironfish/package.json index 51ecc81d2e..96ab2ae298 100644 --- a/ironfish/package.json +++ b/ironfish/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/sdk", - "version": "2.0.0", + "version": "2.1.0", "description": "SDK for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js", @@ -22,7 +22,7 @@ "dependencies": { "@ethersproject/bignumber": "5.7.0", "@fast-csv/format": "4.3.5", - "@ironfish/rust-nodejs": "2.0.0", + "@ironfish/rust-nodejs": "2.1.0", "@napi-rs/blake-hash": "1.3.3", "axios": "0.21.4", "bech32": "2.0.0", diff --git a/ironfish/src/assets/assetsVerificationApi.test.ts b/ironfish/src/assets/assetsVerificationApi.test.ts index 3fb517b4bf..55b11f97e0 100644 --- a/ironfish/src/assets/assetsVerificationApi.test.ts +++ b/ironfish/src/assets/assetsVerificationApi.test.ts @@ -1,11 +1,17 @@ /* 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 fs from 'fs' import nock from 'nock' +import { NodeFileProvider } from '../fileSystems' import { AssetsVerificationApi } from './assetsVerificationApi' describe('Assets Verification API Client', () => { + const files = new NodeFileProvider() + + beforeAll(async () => { + await files.init() + }) + beforeEach(() => { nock.cleanAll() }) @@ -24,6 +30,7 @@ describe('Assets Verification API Client', () => { }) const api = new AssetsVerificationApi({ + files, url: 'https://test/assets/verified', }) const verifiedAssets = await api.getVerifiedAssets() @@ -47,6 +54,7 @@ describe('Assets Verification API Client', () => { }) const api = new AssetsVerificationApi({ + files, url: 'https://test/assets/verified', }) const verifiedAssets = await api.getVerifiedAssets() @@ -66,6 +74,7 @@ describe('Assets Verification API Client', () => { }) const api = new AssetsVerificationApi({ + files, url: 'https://test/assets/verified', }) const verifiedAssets = await api.getVerifiedAssets() @@ -96,6 +105,7 @@ describe('Assets Verification API Client', () => { .reply(304) const api = new AssetsVerificationApi({ + files, url: 'https://test/assets/verified', }) const verifiedAssets = await api.getVerifiedAssets() @@ -111,6 +121,7 @@ describe('Assets Verification API Client', () => { nock('https://test').get('/assets/verified').reply(500) const api = new AssetsVerificationApi({ + files, url: 'https://test/assets/verified', }) await expect(api.getVerifiedAssets()).rejects.toThrow( @@ -127,6 +138,7 @@ describe('Assets Verification API Client', () => { }) const api = new AssetsVerificationApi({ + files, url: 'https://test/assets/verified', timeout: 1000, }) @@ -142,6 +154,7 @@ describe('Assets Verification API Client', () => { }) const api = new AssetsVerificationApi({ + files, url: 'https://test/assets/verified', timeout: 1000, }) @@ -152,15 +165,16 @@ describe('Assets Verification API Client', () => { const contents = JSON.stringify({ assets: [{ identifier: '0123' }, { identifier: 'abcd' }], }) - const readFileSpy = jest.spyOn(fs.promises, 'readFile').mockResolvedValue(contents) + const readFileSpy = jest.spyOn(files, 'readFile').mockResolvedValue(contents) const api = new AssetsVerificationApi({ + files, url: 'file:///some/where', }) const verifiedAssets = await api.getVerifiedAssets() expect(verifiedAssets['assetIds']).toStrictEqual(new Set(['0123', 'abcd'])) - expect(readFileSpy).toHaveBeenCalledWith('/some/where', { encoding: 'utf8' }) + expect(readFileSpy).toHaveBeenCalledWith('/some/where') }) }) }) diff --git a/ironfish/src/assets/assetsVerificationApi.ts b/ironfish/src/assets/assetsVerificationApi.ts index f71049e2cc..a139700157 100644 --- a/ironfish/src/assets/assetsVerificationApi.ts +++ b/ironfish/src/assets/assetsVerificationApi.ts @@ -2,8 +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 axios, { AxiosAdapter, AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios' -import { readFile } from 'node:fs/promises' import url, { URL } from 'url' +import { FileSystem } from '../fileSystems' type GetVerifiedAssetsResponse = { assets: Array<{ identifier: string }> @@ -62,10 +62,12 @@ export class AssetsVerificationApi { readonly url: string - constructor(options?: { url?: string; timeout?: number }) { + constructor(options: { files: FileSystem; url?: string; timeout?: number }) { this.url = options?.url || 'https://api.ironfish.network/assets/verified' this.timeout = options?.timeout ?? 30 * 1000 // 30 seconds - this.adapter = isFileUrl(this.url) ? axiosFileAdapter : axios.defaults.adapter + this.adapter = isFileUrl(this.url) + ? axiosFileAdapter(options.files) + : axios.defaults.adapter } async getVerifiedAssets(): Promise { @@ -116,22 +118,23 @@ const isFileUrl = (url: string): boolean => { return parsedUrl.protocol === 'file:' } -const axiosFileAdapter = ( - config: AxiosRequestConfig, -): Promise> => { - if (!config.url) { - return Promise.reject(new Error('url is undefined')) - } +const axiosFileAdapter = + (files: FileSystem) => + (config: AxiosRequestConfig): Promise> => { + if (!config.url) { + return Promise.reject(new Error('url is undefined')) + } - const path = url.fileURLToPath(config.url) - - return readFile(path, { encoding: 'utf8' }) - .then(JSON.parse) - .then((data: GetVerifiedAssetsResponse) => ({ - data, - status: 0, - statusText: '', - headers: {}, - config: config, - })) -} + const path = url.fileURLToPath(config.url) + + return files + .readFile(path) + .then(JSON.parse) + .then((data: GetVerifiedAssetsResponse) => ({ + data, + status: 0, + statusText: '', + headers: {}, + config: config, + })) + } diff --git a/ironfish/src/assets/assetsVerifier.test.ts b/ironfish/src/assets/assetsVerifier.test.ts index 4261b24448..344e227883 100644 --- a/ironfish/src/assets/assetsVerifier.test.ts +++ b/ironfish/src/assets/assetsVerifier.test.ts @@ -3,6 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import nock from 'nock' import { VerifiedAssetsCacheStore } from '../fileStores/verifiedAssets' +import { NodeFileProvider } from '../fileSystems' import { AssetsVerifier } from './assetsVerifier' /* eslint-disable jest/no-standalone-expect */ @@ -17,6 +18,12 @@ describe('AssetsVerifier', () => { } } + const files = new NodeFileProvider() + + beforeAll(async () => { + await files.init() + }) + beforeEach(() => { nock.cleanAll() jest.clearAllTimers() @@ -27,7 +34,7 @@ describe('AssetsVerifier', () => { }) it('does not refresh when not started', () => { - const assetsVerifier = new AssetsVerifier() + const assetsVerifier = new AssetsVerifier({ files }) const refresh = jest.spyOn(assetsVerifier as any, 'refresh') jest.runOnlyPendingTimers() @@ -50,6 +57,7 @@ describe('AssetsVerifier', () => { }) const assetsVerifier = new AssetsVerifier({ + files, apiUrl: 'https://test/assets/verified', }) const refresh = jest.spyOn(assetsVerifier as any, 'refresh') @@ -91,6 +99,7 @@ describe('AssetsVerifier', () => { }) const assetsVerifier = new AssetsVerifier({ + files, apiUrl: 'https://test/assets/verified', }) const refresh = jest.spyOn(assetsVerifier as any, 'refresh') @@ -120,6 +129,7 @@ describe('AssetsVerifier', () => { .reply(304) const assetsVerifier = new AssetsVerifier({ + files, apiUrl: 'https://test/assets/verified', }) const refresh = jest.spyOn(assetsVerifier as any, 'refresh') @@ -139,7 +149,7 @@ describe('AssetsVerifier', () => { describe('verify', () => { it("returns 'unknown' when not started", () => { - const assetsVerifier = new AssetsVerifier() + const assetsVerifier = new AssetsVerifier({ files }) expect(assetsVerifier.verify('0123')).toStrictEqual({ status: 'unknown' }) expect(assetsVerifier.verify('4567')).toStrictEqual({ status: 'unknown' }) @@ -149,6 +159,7 @@ describe('AssetsVerifier', () => { nock('https://test').get('/assets/verified').reply(500) const assetsVerifier = new AssetsVerifier({ + files, apiUrl: 'https://test/assets/verified', }) const refresh = jest.spyOn(assetsVerifier as any, 'refresh') @@ -172,6 +183,7 @@ describe('AssetsVerifier', () => { }) const assetsVerifier = new AssetsVerifier({ + files, apiUrl: 'https://test/assets/verified', }) const refresh = jest.spyOn(assetsVerifier as any, 'refresh') @@ -190,6 +202,7 @@ describe('AssetsVerifier', () => { }) const assetsVerifier = new AssetsVerifier({ + files, apiUrl: 'https://test/assets/verified', }) const refresh = jest.spyOn(assetsVerifier as any, 'refresh') @@ -210,6 +223,7 @@ describe('AssetsVerifier', () => { .reply(500) const assetsVerifier = new AssetsVerifier({ + files, apiUrl: 'https://test/assets/verified', }) const refresh = jest.spyOn(assetsVerifier as any, 'refresh') @@ -240,6 +254,7 @@ describe('AssetsVerifier', () => { }) const assetsVerifier = new AssetsVerifier({ + files, apiUrl: 'https://test/assets/verified', }) const refresh = jest.spyOn(assetsVerifier as any, 'refresh') @@ -276,6 +291,7 @@ describe('AssetsVerifier', () => { }) const assetsVerifier = new AssetsVerifier({ + files, apiUrl: 'https://test/assets/verified', cache: cache, }) @@ -307,6 +323,7 @@ describe('AssetsVerifier', () => { }) const assetsVerifier = new AssetsVerifier({ + files, apiUrl: 'https://bar.test/assets/verified', cache: cache, }) @@ -343,6 +360,7 @@ describe('AssetsVerifier', () => { ) const assetsVerifier = new AssetsVerifier({ + files, apiUrl: 'https://test/assets/verified', cache: cache, }) @@ -387,6 +405,7 @@ describe('AssetsVerifier', () => { .reply(304) const assetsVerifier = new AssetsVerifier({ + files, apiUrl: 'https://test/assets/verified', cache: cache, }) diff --git a/ironfish/src/assets/assetsVerifier.ts b/ironfish/src/assets/assetsVerifier.ts index b16f89a828..1f3906f2d7 100644 --- a/ironfish/src/assets/assetsVerifier.ts +++ b/ironfish/src/assets/assetsVerifier.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 { VerifiedAssetsCacheStore } from '../fileStores/verifiedAssets' +import { FileSystem } from '../fileSystems' import { createRootLogger, Logger } from '../logger' import { ErrorUtils } from '../utils' import { SetIntervalToken } from '../utils' @@ -28,13 +29,14 @@ export class AssetsVerifier { private refreshToken?: SetIntervalToken private verifiedAssets?: VerifiedAssets - constructor(options?: { + constructor(options: { + files: FileSystem apiUrl?: string cache?: VerifiedAssetsCacheStore logger?: Logger }) { this.logger = options?.logger ?? createRootLogger() - this.api = new AssetsVerificationApi({ url: options?.apiUrl }) + this.api = new AssetsVerificationApi({ url: options?.apiUrl, files: options.files }) this.cache = options?.cache this.started = false diff --git a/ironfish/src/merkletree/merkletree.test.ts b/ironfish/src/merkletree/merkletree.test.ts index 8e1635f3af..0eb3d1d89d 100644 --- a/ironfish/src/merkletree/merkletree.test.ts +++ b/ironfish/src/merkletree/merkletree.test.ts @@ -662,6 +662,53 @@ describe('Merkle tree', function () { } }) + it('calculates witnesses without past sibling hash caching regression', async () => { + /* eslint-disable prettier/prettier */ + // construct tree of size 128 + const leaves = [ + 'aa', 'ab', 'ac', 'ad', 'ae', 'af', 'ag', 'ah', + 'ba', 'bb', 'bc', 'bd', 'be', 'bf', 'bg', 'bh', + 'ca', 'cb', 'cc', 'cd', 'ce', 'cf', 'cg', 'ch', + 'da', 'db', 'dc', 'dd', 'de', 'df', 'dg', 'dh', + 'ea', 'eb', 'ec', 'ed', 'ee', 'ef', 'eg', 'eh', + 'fa', 'fb', 'fc', 'fd', 'fe', 'ff', 'fg', 'fh', + 'ga', 'gb', 'gc', 'gd', 'ge', 'gf', 'gg', 'gh', + 'ha', 'hb', 'hc', 'hd', 'he', 'hf', 'hg', 'hh', + 'ia', 'ib', 'ic', 'id', 'ie', 'if', 'ig', 'ih', + 'ja', 'jb', 'jc', 'jd', 'je', 'jf', 'jg', 'jh', + 'ka', 'kb', 'kc', 'kd', 'ke', 'kf', 'kg', 'kh', + 'la', 'lb', 'lc', 'ld', 'le', 'lf', 'lg', 'lh', + 'ma', 'mb', 'mc', 'md', 'me', 'mf', 'mg', 'mh', + 'na', 'nb', 'nc', 'nd', 'ne', 'nf', 'ng', 'nh', + 'oa', 'ob', 'oc', 'od', 'oe', 'of', 'og', 'oh', + 'pa', 'pb', 'pc', 'pd', 'pe', 'pf', 'pg', 'ph', + ] + /* eslint-enable prettier/prettier */ + + const tree = await makeTree({ depth: 7 }) + for (const leaf of leaves) { + await tree.add(leaf) + } + + // a tree size of 128, a past tree size of 74, and an index of 68 created an + // issue with pastRightSiblingHashes where the cache incorrectly stored a + // hash for the parent of leaf index 68 (at node index 72) + // the conditions to produce this issue: + // 1. leaf at pastSize - 1 is a right leaf node + // 2. node at pastSize - 2 is a left node + // 3. node at pastSize - 2 is not on path from leaf pastSize - 1 to root + // 4. node at pastSize - 2 is on path from witness index to root + const pastSize = 74 + const pastRootHash = await tree.pastRoot(pastSize) + + const index = 68 + const witness = await tree.witness(index, pastSize) + if (witness === null) { + throw new Error('Witness should not be null') + } + expect(witness.rootHash).toEqual(pastRootHash) + }) + it('witness rootHash should equal the tree rootHash', async () => { const tree = await makeTree({ depth: 3, leaves: 'abcdefgh' }) diff --git a/ironfish/src/merkletree/merkletree.ts b/ironfish/src/merkletree/merkletree.ts index a6b0f740b7..eaecf9e788 100644 --- a/ironfish/src/merkletree/merkletree.ts +++ b/ironfish/src/merkletree/merkletree.ts @@ -598,7 +598,6 @@ export class MerkleTree< const sibling = await this.getLeaf(leafIndex - 1, tx) const siblingHash = sibling.merkleHash currentHash = this.hasher.combineHash(0, siblingHash, currentHash) - pastHashes.set(leafIndex - 1, currentHash) } else { currentHash = this.hasher.combineHash(0, currentHash, currentHash) } diff --git a/ironfish/src/mining/utils.ts b/ironfish/src/mining/utils.ts deleted file mode 100644 index 5d77216c0d..0000000000 --- a/ironfish/src/mining/utils.ts +++ /dev/null @@ -1,22 +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 bufio from 'bufio' -import { SerializedBlockTemplate } from '../serde/BlockTemplateSerde' - -const MINEABLE_BLOCK_HEADER_SIZE = 180 -export const MINEABLE_BLOCK_HEADER_GRAFFITI_OFFSET = MINEABLE_BLOCK_HEADER_SIZE - 32 - -export function mineableHeaderString(header: SerializedBlockTemplate['header']): Buffer { - const bw = bufio.write(MINEABLE_BLOCK_HEADER_SIZE) - bw.writeBytes(Buffer.from(header.randomness, 'hex')) - bw.writeU32(header.sequence) - bw.writeHash(header.previousBlockHash) - bw.writeHash(header.noteCommitment) - bw.writeHash(Buffer.from(header.transactionCommitment, 'hex')) - bw.writeHash(header.target) - bw.writeU64(header.timestamp) - bw.writeBytes(Buffer.from(header.graffiti, 'hex')) - return bw.render() -} diff --git a/ironfish/src/node.ts b/ironfish/src/node.ts index 00df62e9f7..e7f7582e2e 100644 --- a/ironfish/src/node.ts +++ b/ironfish/src/node.ts @@ -230,6 +230,7 @@ export class FullNode { await verifiedAssetsCache.load() const assetsVerifier = new AssetsVerifier({ + files, apiUrl: config.get('assetVerificationApi'), cache: verifiedAssetsCache, logger, diff --git a/ironfish/src/primitives/blockheader.ts b/ironfish/src/primitives/blockheader.ts index 933a225e23..4dae43c3b4 100644 --- a/ironfish/src/primitives/blockheader.ts +++ b/ironfish/src/primitives/blockheader.ts @@ -14,10 +14,6 @@ import { Transaction } from './transaction' export type BlockHash = Buffer -export function hashBlockHeader(serializedHeader: Buffer): BlockHash { - return blake3(serializedHeader) -} - export function isBlockLater(a: BlockHeader, b: BlockHeader): boolean { if (a.sequence !== b.sequence) { return a.sequence > b.sequence diff --git a/ironfish/src/primitives/unsignedTransaction.ts b/ironfish/src/primitives/unsignedTransaction.ts index b3a79ddbeb..df644f472b 100644 --- a/ironfish/src/primitives/unsignedTransaction.ts +++ b/ironfish/src/primitives/unsignedTransaction.ts @@ -122,11 +122,6 @@ export class UnsignedTransaction { reader.seek(TRANSACTION_SIGNATURE_LENGTH) } - static fromSigningPackage(signingPackage: string): UnsignedTransaction { - const unsigned = NativeUnsignedTransaction.fromSigningPackage(signingPackage) - return new UnsignedTransaction(unsigned.serialize()) - } - serialize(): Buffer { return this.unsignedTransactionSerialized } diff --git a/ironfish/src/rpc/adapters/errors.ts b/ironfish/src/rpc/adapters/errors.ts index 3e91c6c200..de84887961 100644 --- a/ironfish/src/rpc/adapters/errors.ts +++ b/ironfish/src/rpc/adapters/errors.ts @@ -13,7 +13,7 @@ export enum RPC_ERROR_CODES { NOT_FOUND = 'not-found', DUPLICATE_ACCOUNT_NAME = 'duplicate-account-name', IMPORT_ACCOUNT_NAME_REQUIRED = 'import-account-name-required', - MULTISIG_SECRET_NAME_REQUIRED = 'multisig-secret-name-required', + MULTISIG_SECRET_NOT_FOUND = 'multisig-secret-not-found', } /** diff --git a/ironfish/src/rpc/clients/client.ts b/ironfish/src/rpc/clients/client.ts index b6f92b40c6..a161debb4e 100644 --- a/ironfish/src/rpc/clients/client.ts +++ b/ironfish/src/rpc/clients/client.ts @@ -81,6 +81,8 @@ import type { GetDifficultyResponse, GetFundsRequest, GetFundsResponse, + GetIdentitiesRequest, + GetIdentitiesResponse, GetIdentityRequest, GetIdentityResponse, GetLogStreamResponse, @@ -252,6 +254,15 @@ export abstract class RpcClient { ).waitForEnd() }, + getIdentities: ( + params: GetIdentitiesRequest = undefined, + ): Promise> => { + return this.request( + `${ApiNamespace.wallet}/multisig/getIdentities`, + params, + ).waitForEnd() + }, + getAccountIdentities: ( params: GetAccountIdentitiesRequest, ): Promise> => { diff --git a/ironfish/src/rpc/routes/wallet/__importTestCases__/1p20p0_json_multisig.txt b/ironfish/src/rpc/routes/wallet/__importTestCases__/1p20p0_json_multisig.txt deleted file mode 100644 index 310afede92..0000000000 --- a/ironfish/src/rpc/routes/wallet/__importTestCases__/1p20p0_json_multisig.txt +++ /dev/null @@ -1 +0,0 @@ -{"version":4,"name":"multisig-test-0","spendingKey":null,"viewKey":"24bf7639a6a251c3ea5a1a2083370a861066692627a0417b648052f71402ab214f2b3bee707e1b55bbec15358b2a37099b8bbcdbf8a502f89953a6ab04683b5b","incomingViewKey":"35eb8213299467e1e790f718da8db415427dd0579b567ae45bb4c2107c34ab00","outgoingViewKey":"3988bbb2ca281da03964cef01ffb85f143d9731e17ee4d3010ec08d17659f831","publicAddress":"84f6b03344cf493b4c54966c9ce91023a766148452a6737e04b2d3b4b8d580c5","createdAt":{"hash":"00000004c9c043422c9d9787c2e1a56433ae8760019a2a8d479d42bfd1f83fe7","sequence":192885},"multisigKeys":{"publicKeyPackage":"a600000000c3d2051e0221752510c8264b10bcc513400f2e6986635f8da4f0a5fe30f2cdd544a4ad99090e3a00e8307d803ddd893fe34213e7286eb3f57a5c4d19eff8a18ca95625ecc44fd9b805dbb0883dc26232b4a62493830fb0ca8751053bfaadedfc5c2c7a130a6e67fedac991705bb238f76fc09ab3ddc52ced3268cfd23dd08b844b8b8da69924bf7639a6a251c3ea5a1a2083370a861066692627a0417b648052f71402ab210200000072f2ee7ed629f6218fd67861b08304700cf4d39cd05e02c390dcca75193dd8f0e60db653d41b09fa4ad835c4199f209cec5f29b748da0d69a75c7bc78673f89f2ace80a541a41ca24a2c72bf6d80f0d91083622b4fd583b2b53fbba1afb1919b5af3c106901166de394efa8344591a5d2af4314b787ef0f891642fea79c0c5d304726e9d93a2da40afcf70a60149f81ff321cfbdd9f4bbeecca5c19f07ef2c515315a28a46e68cd0f4a00da076b391ad9cee4a59a9bcd5658113891e889211ff2b441c1127696fe457f97fec0ec5f550cc517d21ae9c05ac7dc91c2f8402a42012f27f222259d98a2488a01d2b1b15724b2179bc2e1d245b7286b58d698cc0950809","secret":"726e9d93a2da40afcf70a60149f81ff321cfbdd9f4bbeecca5c19f07ef2c515315a28a46e68cd0f4a00da076b391ad9cee4a59a9bcd5658113891e889211ff2b441c1127696fe457f97fec0ec5f550cc517d21ae9c05ac7dc91c2f8402a42012f27f222259d98a2488a01d2b1b15724b2179bc2e1d245b7286b58d698cc0950809","keyPackage":"00c3d2051e4fd9b805dbb0883dc26232b4a62493830fb0ca8751053bfaadedfc5c2c7a130ab9ee2d6bdad038ec917cff11d60d47c70fe587ba344691fc4e667b7fcbb054086e67fedac991705bb238f76fc09ab3ddc52ced3268cfd23dd08b844b8b8da69924bf7639a6a251c3ea5a1a2083370a861066692627a0417b648052f71402ab2102"},"proofAuthorizingKey":"efc774fbe4e198d3358f59a960752ef3774dc2fa8f94ffdfc4e2c06dc4512b0e"} \ No newline at end of file diff --git a/ironfish/src/rpc/routes/wallet/__importTestCases__/2p0p0_json_multisig.txt b/ironfish/src/rpc/routes/wallet/__importTestCases__/2p0p0_json_multisig.txt new file mode 100644 index 0000000000..a2bec623f9 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/__importTestCases__/2p0p0_json_multisig.txt @@ -0,0 +1 @@ +{"version":4,"name":"multisig-test-0","spendingKey":null,"viewKey":"293d96b68049736f3cf75eae4b34e0cb7ef4123e0a932d0950ed29993c8f320732217bef15f3b4fb91604456e36d473036b2b71c55ae2792154164849894cfb2","incomingViewKey":"cf53348203c66c3196580d47e32f57e0c9096b4a1e15d5ddc7f4d0c51374d703","outgoingViewKey":"3ceb1aae767b95524ef4352c95ed5e45fb654d9466d22c00157723a3f03d1fb2","publicAddress":"4d740a396b33cea2755342847a3869c1f7b693f5b3839298344c9b0548151c98","createdAt":{"hash":"000000df3bee7c0486981e1c41776bd2e3272a96bf2ca65f5b77a855b8effdf8","sequence":66},"multisigKeys":{"keyPackage":"00c3d2051ea38ba803ede7410f01bba3e18cc043ad504690c88dff3b45d2ba64d05c92c50a02798a08a504390c6d69af81e09eeb310350b743cffdd6d08064c95b4963c10d23caf37a66cf7a18bfb7390b94fc508a5c8c5027c4f1b39f4f0336f9740a8a69293d96b68049736f3cf75eae4b34e0cb7ef4123e0a932d0950ed29993c8f320702","publicKeyPackage":"a600000000c3d2051e02fa165dea65cfb13778b4b6b2ef4fad6045f1ac72e33d122c303ff08b1cf101067bf9c0e2f1cd4b422e930531173f2baaf865b61afaecffcfae20354445635f57a38ba803ede7410f01bba3e18cc043ad504690c88dff3b45d2ba64d05c92c50a23caf37a66cf7a18bfb7390b94fc508a5c8c5027c4f1b39f4f0336f9740a8a69293d96b68049736f3cf75eae4b34e0cb7ef4123e0a932d0950ed29993c8f32070200000072c60942feac595aa83bb0856c264dfc070b8f3c765ca9434054fde21a2806938184b7c5421c5efa9e8f4ba3295ffc229fa05925f08f6ae9837ba4ac6b720a2a44a10c8e761faa2534149c33c7864c4b780d4d83409e5ca3d9428f1f09b0b0476137feb8a8f73cb3d2515110f1b82e88a2af2708a2961ef6c9e15541e288dcad03723729d1b6af022a4c5d67e126a3a79700d5b346577d2ecb1d3b543f4f4f18f2bb43850fb4fd8b554732a11de1b8f0ab65717f01b79c8fd5b459507a65e78c5432454de9de1722f33846ce2bead0b9ad50290edb96b065f2cbc80762518d39a209de1c3e7d998314b3fc9380cdd3b98bd363ec486317bd1e72b8628e61163403040200","secret":"721ac5906f4259f0cf9a37ac553d257a506fe401775f02ab3547391a558bb28ae35dede5d094ad7e3539d523b76636ea61347ebbfc8c3011a4518120f34c3ee2ed"},"proofAuthorizingKey":"c1ecf0f5baa2656f358154decb9011383dccbee08e06f1d9d308d992d03fcf0a"} \ No newline at end of file diff --git a/ironfish/src/rpc/routes/wallet/__importTestCases__/multisigEncrypted/1p21p0_base64.key b/ironfish/src/rpc/routes/wallet/__importTestCases__/multisigEncrypted/1p21p0_base64.key deleted file mode 100644 index cc7cbda902..0000000000 --- a/ironfish/src/rpc/routes/wallet/__importTestCases__/multisigEncrypted/1p21p0_base64.key +++ /dev/null @@ -1 +0,0 @@ -rëONûûP½9¬Ì4QÁOÇz- ©}æ—©­Dolgs —³—¼óì‹.Nò§âCoSÊa©Jû®+ð9$ \ No newline at end of file diff --git a/ironfish/src/rpc/routes/wallet/__importTestCases__/multisigEncrypted/1p21p0_base64.txt b/ironfish/src/rpc/routes/wallet/__importTestCases__/multisigEncrypted/1p21p0_base64.txt deleted file mode 100644 index 72db148ee9..0000000000 --- a/ironfish/src/rpc/routes/wallet/__importTestCases__/multisigEncrypted/1p21p0_base64.txt +++ /dev/null @@ -1 +0,0 @@ -ifmsaccountIC/cMRi3ru9jA53igTvd0bf33195fDYbX7tny064fycBAAAAcZZI2GFnjO30sq8lYqRVAUWlOXC54Y5LDRyvvoMgTfVwAgAANPxPtPMnvHKlk82QvWVMUYOdYaJjKv1kjhXYPzMlQx3IMORM/8UDAy7uM7t0mPDC259ySJ68ACbajWdOcAiQhMBOSNTPmORqAx8moGU4j29SLWaB9U2+pvulpATYozvGWXmlyU1gtZsEoFbHfbcpS5mNmi/mezOp0ugprgdxPkHO2b6aqcrD3kFT1HEc9ErNr8jFqroyLzguRX1/UbnLhiS5VSC5jS4+BGiJ3MnH8gdnbNHiJvxHEQ1JwNNXeFSPwtT3EsGZCFIl2Ca4r6Qq/Li+ETrlGApBdI1wujV6Q3L4PveFn83eRzh0N8hERnr66sdKBWSHQtYz8dsYW8AL3MnSYJR+bvaYsUNFsJDddnATPomRS/Guj3bd/oIo7QOvCsvEwQCUEE3pdflVnUzYz+eOoehCS08ncUojBGvgEaDs+04b6ZE/9zjBi5Y8Lzi4xmrRZZQ7kBH2EU9LhmsQqIo0UlVGdXnem1U0V1nw15x43lew33p/ZgrxaVtX1CI2C2Sq+N334ffztXrSP3XEJLLHX+fYH1XsV1E5EORoM9yYqZjw/JLuByXiCmEomyxXEvhFwyHiEUkVrxk9MjDb/KW1f04R67lxMdZq4aEW0peoGIiZv+HGTBNx3fms47+pGjuMnJgP99f55LWgZBXK7xrAqbQ67bgRcqJ/2yV0UxEl/LblHlC7PuVIpml3buI10EzgSk6yu6gTJ8pyuVO0HY6CTgAzJBrvxF4PAxDtsZwv4+M6+IZc55TJ07mk0rlHPw2TJiYkIn1Bh+J+O8qvR2q0PsJVYLwJYNN16MBdoL/gF7xDF+sER7tm8+cIdOGs \ No newline at end of file diff --git a/ironfish/src/rpc/routes/wallet/__importTestCases__/multisigEncrypted/2p0p0_base64.key b/ironfish/src/rpc/routes/wallet/__importTestCases__/multisigEncrypted/2p0p0_base64.key new file mode 100644 index 0000000000..585ae400bb --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/__importTestCases__/multisigEncrypted/2p0p0_base64.key @@ -0,0 +1 @@ +721ac5906f4259f0cf9a37ac553d257a506fe401775f02ab3547391a558bb28ae35dede5d094ad7e3539d523b76636ea61347ebbfc8c3011a4518120f34c3ee2ed diff --git a/ironfish/src/rpc/routes/wallet/__importTestCases__/multisigEncrypted/2p0p0_base64.txt b/ironfish/src/rpc/routes/wallet/__importTestCases__/multisigEncrypted/2p0p0_base64.txt new file mode 100644 index 0000000000..6fd8ea279b --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/__importTestCases__/multisigEncrypted/2p0p0_base64.txt @@ -0,0 +1 @@ +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 diff --git a/ironfish/src/rpc/routes/wallet/exportAccount.test.ts b/ironfish/src/rpc/routes/wallet/exportAccount.test.ts index 0defc77f95..7a77177206 100644 --- a/ironfish/src/rpc/routes/wallet/exportAccount.test.ts +++ b/ironfish/src/rpc/routes/wallet/exportAccount.test.ts @@ -201,4 +201,66 @@ describe('Route wallet/exportAccount', () => { }, }) }) + + it('should export an account with multisigKeys and without multisig secrets when view only account is requested', async () => { + const accountNames = Array.from( + { length: 2 }, + (_, index) => `another-test-account-${index}`, + ) + const participants = await Promise.all( + accountNames.map( + async (name) => + ( + await routeTest.client + .request('wallet/multisig/createParticipant', { name }) + .waitForEnd() + ).content, + ), + ) + + const multisigSecret = await routeTest.node.wallet.walletDb.getMultisigSecret( + Buffer.from(participants[0].identity, 'hex'), + ) + Assert.isNotUndefined(multisigSecret) + + // Initialize the group though TDK and import one of the accounts generated + const trustedDealerPackage = ( + await routeTest.client.wallet.multisig.createTrustedDealerKeyPackage({ + minSigners: 2, + participants, + }) + ).content + + const importAccount = trustedDealerPackage.participantAccounts.find( + ({ identity }) => identity === participants[0].identity, + ) + expect(importAccount).toBeDefined() + + await routeTest.client.wallet.importAccount({ + name: accountNames[0], + account: importAccount!.account, + }) + + const response = await routeTest.client + .request('wallet/exportAccount', { + account: accountNames[0], + viewOnly: true, + }) + .waitForEnd() + + expect(response.status).toBe(200) + expect(response.content).toMatchObject({ + account: { + name: accountNames[0], + spendingKey: null, + viewKey: trustedDealerPackage.viewKey, + incomingViewKey: trustedDealerPackage.incomingViewKey, + outgoingViewKey: trustedDealerPackage.outgoingViewKey, + publicAddress: trustedDealerPackage.publicAddress, + multisigKeys: { + publicKeyPackage: trustedDealerPackage.publicKeyPackage, + }, + }, + }) + }) }) diff --git a/ironfish/src/rpc/routes/wallet/exportAccount.ts b/ironfish/src/rpc/routes/wallet/exportAccount.ts index a0758cd71f..42b293b24e 100644 --- a/ironfish/src/rpc/routes/wallet/exportAccount.ts +++ b/ironfish/src/rpc/routes/wallet/exportAccount.ts @@ -46,6 +46,11 @@ routes.register( const { id: _, ...accountInfo } = account.serialize() if (request.data.viewOnly) { accountInfo.spendingKey = null + if (accountInfo.multisigKeys) { + accountInfo.multisigKeys = { + publicKeyPackage: accountInfo.multisigKeys.publicKeyPackage, + } + } } if (!request.data.format) { diff --git a/ironfish/src/rpc/routes/wallet/importAccount.test.ts b/ironfish/src/rpc/routes/wallet/importAccount.test.ts index a01b3aa28a..e4b98f5968 100644 --- a/ironfish/src/rpc/routes/wallet/importAccount.test.ts +++ b/ironfish/src/rpc/routes/wallet/importAccount.test.ts @@ -1,12 +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 { - generateKey, - LanguageCode, - ParticipantSecret, - spendingKeyToWords, -} from '@ironfish/rust-nodejs' +import { generateKey, LanguageCode, multisig, spendingKeyToWords } from '@ironfish/rust-nodejs' import fs from 'fs' import path from 'path' import { createTrustedDealerKeyPackages } from '../../../testUtilities' @@ -344,7 +339,7 @@ describe('Route wallet/importAccount', () => { it('should fail to import a base64 encoded account if no multisig identity was generated', async () => { const name = 'multisig-encrypted-base64 (no key)' - const identity = ParticipantSecret.random().toIdentity() + const identity = multisig.ParticipantSecret.random().toIdentity() const base64 = encodeAccount(createAccountImport(name), AccountFormat.Base64Json, { encryptWith: { kind: 'MultisigIdentity', identity }, }) @@ -360,7 +355,7 @@ describe('Route wallet/importAccount', () => { ).rejects.toThrow( expect.objectContaining({ message: expect.stringContaining( - 'Encrypted multisig account cannot be decrypted without a multisig secret', + 'Encrypted multisig account cannot be decrypted without a corresponding multisig secret', ), status: 400, }), @@ -373,7 +368,7 @@ describe('Route wallet/importAccount', () => { await routeTest.client .request('wallet/multisig/createParticipant', { name }) .waitForEnd() - const encryptingParticipant = ParticipantSecret.random().toIdentity() + const encryptingParticipant = multisig.ParticipantSecret.random().toIdentity() const base64 = encodeAccount(createAccountImport(name), AccountFormat.Base64Json, { encryptWith: { kind: 'MultisigIdentity', identity: encryptingParticipant }, }) @@ -388,7 +383,9 @@ describe('Route wallet/importAccount', () => { .waitForEnd(), ).rejects.toThrow( expect.objectContaining({ - message: expect.stringContaining('Failed to decrypt multisig account'), + message: expect.stringContaining( + 'Encrypted multisig account cannot be decrypted without a corresponding multisig secret', + ), status: 400, }), ) @@ -414,8 +411,10 @@ describe('Route wallet/importAccount', () => { }) const keyFile = testCaseFile.slice(0, -testCaseSuffix.length) + keySuffix - const key = await fs.promises.readFile(path.join(testCaseDir, keyFile)) - const secret = new ParticipantSecret(key) + const key = await fs.promises.readFile(path.join(testCaseDir, keyFile), { + encoding: 'ascii', + }) + const secret = new multisig.ParticipantSecret(Buffer.from(key, 'hex')) const identity = secret.toIdentity() await routeTest.node.wallet.walletDb.putMultisigSecret(identity.serialize(), { diff --git a/ironfish/src/rpc/routes/wallet/importAccount.ts b/ironfish/src/rpc/routes/wallet/importAccount.ts index c992541a9d..9972336561 100644 --- a/ironfish/src/rpc/routes/wallet/importAccount.ts +++ b/ironfish/src/rpc/routes/wallet/importAccount.ts @@ -2,15 +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 * as yup from 'yup' -import { DecodeInvalidName, MultisigMissingSecretName } from '../../../wallet' +import { DecodeInvalidName, MultisigSecretNotFound } from '../../../wallet' import { decodeAccount } from '../../../wallet/account/encoder/account' +import { BASE64_JSON_MULTISIG_ENCRYPTED_ACCOUNT_PREFIX } from '../../../wallet/account/encoder/base64json' import { DuplicateAccountNameError } from '../../../wallet/errors' import { RPC_ERROR_CODES, RpcValidationError } from '../../adapters' import { ApiNamespace } from '../namespaces' import { routes } from '../router' import { AssertHasRpcContext } from '../rpcContext' import { RpcAccountImport } from './types' -import { deserializeRpcAccountImport } from './utils' +import { deserializeRpcAccountImport, tryDecodeAccountWithMultisigSecrets } from './utils' export class ImportError extends Error {} @@ -51,11 +52,17 @@ routes.register( let accountImport = null if (typeof request.data.account === 'string') { const name = request.data.name - const multisigSecretValue = name - ? await context.wallet.walletDb.getMultisigSecretByName(name) - : undefined - const multisigSecret = multisigSecretValue ? multisigSecretValue.secret : undefined - accountImport = decodeAccount(request.data.account, { name, multisigSecret }) + + if (request.data.account.startsWith(BASE64_JSON_MULTISIG_ENCRYPTED_ACCOUNT_PREFIX)) { + accountImport = await tryDecodeAccountWithMultisigSecrets( + context.wallet, + request.data.account, + ) + } + + if (!accountImport) { + accountImport = decodeAccount(request.data.account, { name }) + } } else { accountImport = deserializeRpcAccountImport(request.data.account) } @@ -70,12 +77,8 @@ routes.register( 400, RPC_ERROR_CODES.IMPORT_ACCOUNT_NAME_REQUIRED, ) - } else if (e instanceof MultisigMissingSecretName) { - throw new RpcValidationError( - e.message, - 400, - RPC_ERROR_CODES.MULTISIG_SECRET_NAME_REQUIRED, - ) + } else if (e instanceof MultisigSecretNotFound) { + throw new RpcValidationError(e.message, 400, RPC_ERROR_CODES.MULTISIG_SECRET_NOT_FOUND) } throw e } diff --git a/ironfish/src/rpc/routes/wallet/multisig/__fixtures__/createSignatureShare.test.ts.fixture b/ironfish/src/rpc/routes/wallet/multisig/__fixtures__/createSignatureShare.test.ts.fixture new file mode 100644 index 0000000000..3aa43c122e --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/multisig/__fixtures__/createSignatureShare.test.ts.fixture @@ -0,0 +1,52 @@ +{ + "Route wallt/multisig/createSignatureShare should fail if signing package contains commitments from unknown signers": [ + { + "version": 4, + "id": "61bf039a-c1b9-4e3e-81cc-dbf4941e5abf", + "name": "test", + "spendingKey": "a484355098779a9f23845c462f2ae19d68e36539d78e1a406366d42907312f91", + "viewKey": "c011707e642d47b8ec0dd30bc7f9d76492c41d3664a88d009b427a57aad0aae1b8eb713ece06e1da39339ae03142fec80d06d56c174c66b92ad60e9e9b6fd418", + "incomingViewKey": "bac645c41ee0f5505dfc900dab0aa1d771c8642c764238ef2d72b019924bfe07", + "outgoingViewKey": "44c84aff5511b957f4f8fa240771f34d0ec686a6100ad0fefd69f2b9c6c65df6", + "publicAddress": "10ed4ae03abbb214ce2c770e68d676790577354f6d9a6778d816a1f27c0c343b", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "proofAuthorizingKey": "777e899281aa345157af657fd46fd0db7021b361e77482bdd77bc8b94ef9d802" + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86", + "noteCommitment": { + "type": "Buffer", + "data": "base64:tB06dYIsy13rye2opzCnL2kpYtYZdknJ7lLFiI6SMTU=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:S8V7ZbzVBHv7U8YmUteaGsu7qisygx+5nnhgR13Dfho=" + }, + "target": "9282972777491357380673661573939192202192629606981189395159182914949423", + "randomness": "0", + "timestamp": 1710287344011, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAtculr8Kmmhm/20PSjNsANrRmVWXMxq1DenWUa0xZXJyAmx9IuRVhXXQ8b5QgG5Xj5mXOLr0tBWIYF2OO81RX3Q7SwdUE4qonwafubi4H5DmhgQ+btCqEnrxIf9naGooY7+ooOqsrlB/ejHErClmbHHgEfIP4DD6UZ7PqrWNb1bsBrvdVpI6aKYivL5ORleDoH1vc2PCmaRP7/vph6BUB0s0Fr4f5Y1lEh4OOZbnj/miy17Vwpw6lkRJM60jPL81a8EmAE80PGTnjQ4dFxILNrNd6fHOUoxVYQYr97X7XkyGl2WbDzpQadsQf2HM5RQfos67/5zhu4siDUA54HOK7pArYw59/oU9yS878zcfi8BAowOf7GbsenoJ2PPTc/VsJlg1j2f8S0oeBMdYRfO0Yzov+46vH6ltiuU5XjYHK2YqnKbgDeCFpR4jGrdQXLnwD0sLZ1R8k9GqDzXjuPgpn7PNObEeWvr67C1DHhgsuUg3+o17QSR8cj0Php7db53ord1wJZP/iHWTDXx4Nj4lSz2Z2qxKWvIuB0P22luEL7pkcVJZTk6wl7N/tQvK1YpYxIWBmK1wSww5C7fvaMlCb6e4SShptpiGXglxgY4NVIebFVXy5vHC1v0lyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMFakCBeWP8ytzn5XSmmKUjbqF89rY1/y3cXnlvwNkF7AGT2LjdlGiYLqr5jynH5h98TkD5qYDirgdZZKmSvXAw==" + } + ] + }, + { + "type": "Buffer", + "data": "base64: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" + } + ] +} \ No newline at end of file diff --git a/ironfish/src/rpc/routes/wallet/multisig/__fixtures__/createSigningPackage.test.ts.fixture b/ironfish/src/rpc/routes/wallet/multisig/__fixtures__/createSigningPackage.test.ts.fixture index 0027f82258..8f22b832d8 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/__fixtures__/createSigningPackage.test.ts.fixture +++ b/ironfish/src/rpc/routes/wallet/multisig/__fixtures__/createSigningPackage.test.ts.fixture @@ -2,13 +2,13 @@ "Route multisig/createSigningPackage should create signing package": [ { "version": 4, - "id": "5bd50c3c-6539-444e-a65f-7576df689ec7", + "id": "50b67b6d-8a54-450b-b9af-50f8651146fd", "name": "test", - "spendingKey": "fe1e0c55cf4a6528b8c0a7765c98be0e895a6744500593ba9c796507322a7dac", - "viewKey": "f461e2eda7d920c3eec1afca71eac6fe176d61bc5e72778a5d51189fb0f5a44b43643eff7a3f29f6c76ee8e0c4e800f1aaf9febeffac26cc08d36cecbaacbc2d", - "incomingViewKey": "9bc32b94f00c3bf8198434a1ba7a501d8c9b64423cc5c920392e7ae038b74e00", - "outgoingViewKey": "c20bd7e169306324e3050087b8f1a44795bd5866e227926b8104872f6bfd46b9", - "publicAddress": "0531c3796cd0e0e5db4118ad16662413c3f41b490cf51f531d94a3d04f3821cd", + "spendingKey": "8322f28377136d6bbd25eec41c5db1176a47ff8f2ac0b45e509717d396dd3711", + "viewKey": "060aac7f398ff10ac221b0a19d2548bb273f27a0cf79ffde85a563b8f3d81f59b768f016dacbd72b1f3091694047aa4a8dfdd7c3f41d1fa87e240d677b4e1899", + "incomingViewKey": "75be181d6151a86d6612e4e35b83a7227aceb62e5f5ebb191bb9cd0a5c5d4107", + "outgoingViewKey": "9a8e0acb94da01737b2a5be65dce398082c06f54330a82699fd4d8197f6a17cd", + "publicAddress": "9413d3e00d7ca35eb28f650acc37e4ff07baa92cd12c23f8235649898b44e3ad", "createdAt": { "hash": { "type": "Buffer", @@ -16,7 +16,7 @@ }, "sequence": 1 }, - "proofAuthorizingKey": "cfb8683f14041deb798c49c9d704e8ad08b50967f847b9b6ae4b4e9ba84c640a" + "proofAuthorizingKey": "4ceb6b9c04c69cf692a8a56700557d4baebd41ec389acfd52682ecfc03511502" }, { "header": { @@ -24,15 +24,15 @@ "previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86", "noteCommitment": { "type": "Buffer", - "data": "base64:0NKojeVsZLsIciUPhfVX+PmuFh73r+MXTo29XXdFEBY=" + "data": "base64:0EdKov9dcUfMYbiE+6PUVxmr+oD/4bMTBPidIRhSAiA=" }, "transactionCommitment": { "type": "Buffer", - "data": "base64:prOD+qubJ06SCDKIIHzBgU56ivmiZca+oxnqgfhiH/g=" + "data": "base64:3V3KvI613pMMdrglnV1P+Y52Auqrdo4ehmWeL+UfheM=" }, "target": "9282972777491357380673661573939192202192629606981189395159182914949423", "randomness": "0", - "timestamp": 1709591815765, + "timestamp": 1710203895214, "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", "noteSize": 4, "work": "0" @@ -40,25 +40,25 @@ "transactions": [ { "type": "Buffer", - "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAADViTHO69UYSpUNzZtsYC7iL8+75ckf7EXC/dKMMB+nGsHPEK1i0FZTjIvT9udrBhfEQCxL5iaY4CMRG5JcP+qEHjCPM84bVx5Y66n9Lav1KS+8h+5Hpy185DQ6seI4TadSCzTbnDgW+f+vEwISh9l0tCw2cnOpthfy16lvTzOm4JbglshDQpaYdJlyAojswIoasccBPrwLlAkqRrzt9UE0rx5AmW/Y1SywQmDrj1tiCZHjcV5M+hQKnSed484Glp1od2cZLTsiy/XNULj8Uwp8PlGK3bfPfudwCq8sSXVDFRvbgUirh0BIbEEjg+nn3gDkYLqvorUSXbPGQjU/SabvKyCU5gxXsa9o+Hsn+7F+9roW8r2OPgfRuUYTaXulkOfAZokFtIipHNUUMsgOOfWFMlTVbO3E+m9DjSZ710vW0yMXigoUBcHSFgGl6IaljuMUqA9VR6byASLC0zXd7wk+sxS897RfJpPT9OMXuHZ2ueYteat8G1N35CPOnGM3KubMfrw0l8qeXoIFeyAJ0+Spa+R0PabzIZj1VnT88aUR2WyfQcwRorwca7eSPkugGXbIsqmx076glhcqqaYC7rubphcDCiiUPIv/B6EPIe8i5UcOIJd2EMSklyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw842XiOqgQYieqFJCCK7D7CHaDCmEkuNF/o8RGe4qt1T7AVXhcIG3e3AXib8Q6f1LyWhSjbBkRzlqH5gT95paBQ==" + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAA41JEalTDDdlUGDlyJNQ7DJjl5skgZC8dzG49ZNMAVraIh8xKLNjjoR9Y4P3rl0BlN6mwjfJTRJhEGXGB7hWFoSi+wgt9SJxjUtwy7lfqc1CU4pASyQM7FE2MbICkhsdCyBqlQ62TRNJnd8fMsVLulXzPPNsMdSxdhpczjPSuD2kPvnXoRdbm4zCBwAiPkwfVcnVFaN4t2lHo6tIdlMvuzku98HFSs1YVQ2ZV9EiPlhWnY6d/SFjYZa5dqKpxT/np4alPfopgsj9YJzCECDl8+gTgaOZxlskeTHpgfD3vOpkAk4Xb4ewEHSk7twczsLMHjHw5nB5xdeN7FqKyrf++ATiWDiy/RJM5xkK0DE5ecxm0iwkjuc24icyDHxnAl6ASKk5tyNin6yViiHQELg9Fb85Or/LIwIlPaerhrCyz0ucD2mRg9hPFOEdoBHOd4cbORfyHazngsXm2Ab7aRp/H17KTDgZo9vc7h+Xi0Vs2VblG9D8SIzNjzd8M1O3Y7bMyE0u0TrVn61T6jMxSpISqqlIdswwqQ1JS0xEnhtmqM56sfTyL2aSaGAnoVCQ2LjevXiPmZ9sAEGCOZ+UWdpDFVQWNPGRtCGZS4E3fwFSpE59l+zcJ9W7y0Elyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwqCJMp9pcsFW6EselDQELfkL9v8Y1W7eMkHOPaJDuppyHI70s4W3a+rlCprnHsSOFtcVrnMD8I9tRln+Ud1VoCw==" } ] }, { "type": "Buffer", - "data": "base64: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" + "data": "base64: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" } ], - "Route multisig/createSigningPackage should verify commitment identities": [ + "Route multisig/createSigningPackage should verify commitment consistency of identities": [ { "version": 4, - "id": "453094d4-6348-48a1-b625-75f68461fe72", + "id": "9bae7509-3fc2-4cca-bdb3-ab79dc43c67c", "name": "test", - "spendingKey": "4f0ec9dbc204d53c3e960f0cf935e11e08ed00a11bfaac2fa043dd67dff37ef8", - "viewKey": "27b0cbbf1bbb319fb83fd6b423bd64872ddb9f97dbd7b53a40e65f98cf320f4c1d2f5efa957061251cb560e506327363c2353ffdc03d9aa7fbbfc19c4bd0e0a4", - "incomingViewKey": "c6e34c9247139a84bb4336f61a7ee32b3f151c983e0f717f388db6598718f204", - "outgoingViewKey": "631c569fa8f09e783a238c21bbc3cbcffe0aa482a7b35d507958e10b18009e05", - "publicAddress": "e2bdb8edaddf79665a291557806198b91261d93a28de4bb81334ed90b4875445", + "spendingKey": "920715ad9ebbedb8ddb6def1440c080fcc4ebaae5ed97bedd1aa65f4461334c4", + "viewKey": "2c1e157087a207516ef2f941bab0809c34223609f44a6569b2dff8053aad0f1dcf66e488ba7155bfd9ff921a01e87348dff9a19e8095a7dc7f9d21fcaf907a1f", + "incomingViewKey": "ccdd6ede898c9f76dcb856d3e31301beb57292597cd0a6f87f4647d5290aa200", + "outgoingViewKey": "28b454f8f451d895260fda3fc3e4ce11410880d26bdfa56213aa13d41fd6fc68", + "publicAddress": "b2b06f5534985b031661ab403079400a6a4398f3f1d54c0d907bc88a4d91c217", "createdAt": { "hash": { "type": "Buffer", @@ -66,7 +66,7 @@ }, "sequence": 1 }, - "proofAuthorizingKey": "fc4f4bbb17d69002b1cf28de960645ba5ef26d29d9a54dee457c9d8d9b164b05" + "proofAuthorizingKey": "2cffae476b9ec2f8d2d506c187406123b66f2456ddd0a1d03f9c10244dab470b" }, { "header": { @@ -74,15 +74,15 @@ "previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86", "noteCommitment": { "type": "Buffer", - "data": "base64:9BbvmAKv3VDcfPSUfr7JPoS3JE0upFIirARc091abgg=" + "data": "base64:7eU3qQYIUzZ5TzhxkHebLF+a671BuV51/7/3tkNWLQ0=" }, "transactionCommitment": { "type": "Buffer", - "data": "base64:0P7hihq6K71YDMYpa/JKZuBoPeq6en2WpdpREIscfNQ=" + "data": "base64:CHP2Um3A7uEVY6RTLadBopGKRdLueQtEeYv97Ly5Y90=" }, "target": "9282972777491357380673661573939192202192629606981189395159182914949423", "randomness": "0", - "timestamp": 1709591818640, + "timestamp": 1710203897374, "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", "noteSize": 4, "work": "0" @@ -90,13 +90,167 @@ "transactions": [ { "type": "Buffer", - "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAEFCQ6P/SypxBxhs9f0L8NVdta8XhV0B1NAbs8YNTrQGUY5IWm0YsBIEddvJ/fc83F+VYvICguW4N+HZfp9znMT639wCeM6v+PhebnVAZpKuMBevQ9kFhpVdPStPfjdSCb+KG83/svf3fLD0SM5Lc8rsvyvLqQphEZ3cG1aWuG/YWPye+0kYUAEEOpeG/ybau24N2FG5VLk9S1y05q7kjkel/wqah9qydOuxy+TlRQJ6pkXfS6XU8hnAamhQ41+hDWTnttRHu/RjVyRiYCFQsXGmzHy9iQdkgG4D93+EiCYqELTE0p3o5g9F/88kwVcn2EQ/uKBqPlBFY9QdgPDnKieatXC0XdxTK0vXAu3vUfXChJ9OKQJeNN9go5HEfNsQBiQLOad0ruMnWs/2qd8/0807IF8LGvnZRpgc2XWmEmI+Gn4vu/sddMNqGl/uSr5KH7XPZbDfrmqVvfOBNN0MlGnkePpFmNTQGcx4MDKYg82+Wav4I9NgrTwyJ6Hs4OeJHRnh74c6ZPvSTOJGmxhwq7JifEQA0USRBHX17hr4SFqgKqy5oHCVstvFWPiPqdO5ErxtWWxNJZN9LDA/MtvBdDXxT/Itljt54bGi2zAlM27aWWewebN+9tElyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwugkZaNFTWRvmGTppiQTRIpa5WTA0BUttHJjwHfbiKzTSfbF3i4rBJFbdP/BEBz5MoJK94Lf2FS3g4rl81c4pBw==" + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAD/skSeTarqCqe0vjYTUvCoawVtnPDhPHtPpjqXK6wdKv2wxJAE64Auyklx4Y3uX8RztZ0ckw3/+Mjg1/51HYUTVpKLMmCpf1rEvzQnvV92m3jSj+IzuEsTZRkljD/pIX1CdVMYRBnh/XzNsVIzRzGEVplkwfEqkQpSpUux8E+JgF3Ti0AZAznSB1c6+x/Wmh5qe1uw/77V+C5KBroeWjy5NFUAIYZ9u87EsFBzv2BZCBhyI2LkfEaysqiP9ubrOKgn+onVA/RZm8B5z65XdHI3nnSLhwIYFSQDD8PQmI0aergB/Uz8SP7x4u2H7V3vChcQjwjVgVCY6DyhC4y7Dml5CVTXRGGgt3YCZ63AviT2osCh4qcskNOvQOl/5NMMIOjLifaGvA8IcZuG11KJaBU4INPGExt99M4OlI7fyJETIWKF0cY3+aBCC/WTefDkLruW8QZRuzbFpwDksXa4RdQJ3dIVXCAtZxDghjEdP+TNP7RPBLvxYRVcgHPcMMwTj4wJry3VS7Ok+7W3S9E5H2k1TMcYYKPGSXTR3xrNZ6kR1S8+cnU1hrU29JPARVEMt8/pSWBBKjXJbdgIJXj3VsXFeAlTjiA4gRghJgSaJbEM1JSz5h5YeFZElyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwlp5gzQJP+7MqrO86KBbpf6dCN8r60DlUmPRF24xcy1cGDYrj+svj3naKTkRvvpf+ToosXMPzELJhIWqvm+4+BQ==" } ] }, { "type": "Buffer", - "data": "base64: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" + "data": "base64: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" + } + ], + "Route multisig/createSigningPackage should verify commitment consistency of transactions": [ + { + "version": 4, + "id": "0379b1fd-2499-4343-aab9-c64f4906a4e1", + "name": "test", + "spendingKey": "f30290a4aa6b9cf9453d6b1fe8c7cff10c5fe2e4cfdbd3f9d215e0b413c437bc", + "viewKey": "ebb379cf8516ea35348150b3265926f53f11decd0b87c4827abf328f92f8830af566575b7f2268bb1a2a498df062b70711ccc3d6f5f69de0fac42bccce828a23", + "incomingViewKey": "db71bb9324a0ee17fe3e03d6d65c9e2ffbd2a4eacce9309953429e81bc453400", + "outgoingViewKey": "4fb5cd86117e7afb274a044bddbb91a4b00be2c6c5ae677e800b5a4efd72e154", + "publicAddress": "04eabb613609395afe1ece0b2358a4d01a341712f9a6ac30975600c327a94829", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "proofAuthorizingKey": "10dfc1d93f218884408853665ae561ee2de8af5f3b0a6773f759ab8a8c0d7308" + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86", + "noteCommitment": { + "type": "Buffer", + "data": "base64:GqH3sVMbIt5MAryBPNQqs0ZqgbGh+rJlBGt3A02Mdyw=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:JO8B35op0DI5hSzoGFvZVoDfPgN8zR88w9oOEf+lnTo=" + }, + "target": "9282972777491357380673661573939192202192629606981189395159182914949423", + "randomness": "0", + "timestamp": 1710203899545, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAMiNfmRNruNZf+R8/c4FkodqwywRUWVYNcD+/4s5mk6m1q1ndYqHzABdgLzKUxsQkvBwLfSacoVsnbT4/fjou2wO9hzXZ8mFKjLRp02JqoFiOCpy1PMxabkUkiP2/Zvh82qkW/ABiyMcJikBUL7XMu5/4iiCkABv0I1qjVOgdsCsKU3ShA9z22BAqSIszYjqe8m5WBa2bX4Gg6AjXv27PE7pjN1capbI6mNoMcINYpnKMdW9lIO7Z4tUxWVv+uHyLbtrvKZG18NjJC3kPywYwXxAwZV82hIG47GqZZ02kU6z2VUzm8lz/q/cHZcBgflWGvhPdqK/kX1+PkINX5DgTCSIYF4oe5j3LZgg6nheI/f7vOTKIyqADiTYnqjCyEY0YWTkjEGqkoU4K6wLcDf1BSEboONYGELIXbBLcaqoMSm53uAmhhThKQ3MPEGZmqkPZQBHj754EWY2t9j9ssZaZ9FnGlJpr5PFeX+ljFZ6t+Jak+BL39grw5q38tkAUVAjsiBBWLOUSmWD5dJEOOhJbTm2nBzFyEFhNEvpFr2yyxAoYdOaLNroh9T8c47CDMOp4HfmRcp2DZZESDs8UdJ+yPzeZOKke7nvvirCtWYvO7NBASrCCrzJ2Q0lyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwWT246C2oy8KoJR+y9tTWCNkpvcoKXg7NoghrP6Wgde1zVSMI463Y7bTyJDUe6OkZo1vg+jj8riO6wAzCCC/bAg==" + } + ] + }, + { + "type": "Buffer", + "data": "base64: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" + }, + { + "type": "Buffer", + "data": "base64:AQEAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJLoontIf2ltnvTEjDKxn2eZ8DFwG2IA07MF44MPpXIUezLLswDCdab9uCIESoXlgEwOLVOuVXrdQO+xs6erwDR7MsuzAMJ1pv24IgRKheWATA4tU65Vet1A77Gzp6vANjZ3kCc/ddIZ0q0jp5k0X5k9YHBYPxq2aUe0ovMrm+w7Yz8RO2ttQnzXapjXHFd14mCzBBmIl7/mAQ9LHGQGXjvgBvxpiMnxkbl7xu5n0XWU/Knu43AkHZVYiYKriSlFmFQuJUDZUVl1qnUK6whWJEQV/e+HFrDNrY8BNxPiGTpWqZlSBSdo6kEJFtttbxboBpEDYi3oOsOvlNNgae/9J9OKTtuc4RarCaSmnHRTIRSGKWtX4e293yMO8XVhtvEOsbIYDxhgI6kBI2Y6MupD0uq8FpwD5dZS2qu86KK/9xxMaofexUxsi3kwCvIE81CqzRmqBsaH6smUEa3cDTYx3LAQAAAD7qHrJv/0EaM4zffGMss46WbwIOo4DwcQ5dLYJa1ep1QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACN9uN7l7z0WWd8eR+WykC7UfiHbEJlIuC/SXgPg2KSj0F4La+BrXThqHHZZcRVRRaPPKWGteYazPV9qTpjRnMFvi3ZYfMMAFCdY+gdg2WvsFVT0WiRFLL74llMPIyizM0BhjidHar3AklSYmCmAT1f07LaAr59f5I568YbvetE5JlASumhy0jtRH10fHvctv6F6sksuAgDWV15JZWBX5phF91hNZlr8Ttp8Y/Jr18jTYZG7qBCh9ToYCq/mco4blc/VEnG3j+EqukjPXrr0joNw95U4RbNz4iw+/V5/6KFyzeUfkCbWIgKpiZi8PX3eEP/78pT+83T2IhCMbeC748AoDh49c/JeC0obGNUKQg2MCnTkYxEmBiTvZxSJXC6dUAnse34KUlDUc22ZYDlIs51hBRiccy7IXT4hVGXDHO68Pe2RoXmNxR9Rus29/Mu2RBEWcfJWXbZ96ipSsi+xK7TgaNvIpQ3KjMU7KKlfPqRnd3Byo87P8D7OCJNP7HVeXK9ItUW2yF1e+bpG5hLt0fhXm/dfXUYMbRuT+BjE7jN8OcAABw3IvzabaRM/njeGzn+IcfU0JIfF81cdAB+m/zs9DJyHSlRfbCBpkZaWsvVwvOvF+sXiJGJQkXnavdLLNyeQ/7kO0DlyaRag+6ZNn9stdAqDE+Qb7Py/ueV+pRIJo5UTenx1yQxtlSrIepYI2nM39+TnKnQ3j+s0I8i6MwRQefOgtFKhGXkxfdshURBV/jiOs59Be9LrfzjwXnzv28prdmt8BcgvNs87mxp+P6gc/kuee8r2WXW9O8KdUTv68LC7qN3FKgJEs45ugMXgRpn4IugykN1DSldYZTbnhx9azr92OLTXYPoWGRJ5tGZEYqVChAy1BlVprDi/FsecMmCo8PuygQ7kscTkuGcX5c3HxWCus7TvizYHeSnRizGNGrO+2kJuKuuNGqVh4CDGFaJgu6RLpPkKC6F3mEWZ04AElWKIZ29TreHvFIgVTMJER16A10kHLfFO23gzF2OS9fzpUaNfXWCJyWwQvtwJh4z4UXFh3VvvAM8JafR9iLrodrBqxeSQ8Bg1ZY/KQ92EwtJdpu2RDff1KD0FiC3mm6HwNgw1Fyc+ewDh0Tsmtbhtx2O2BfKUDbt1CcYjKrrsppuSd9EYaUZ6mRKUcmvZa7EwmaiEtkJauKQvy1TtyfPSi6c7oPZn03A9UBMSwqhgVEg35orJdqau5TvOrYQN6hB3A4jnxKiwLM4wQFfOYhu9Iw08Y832ElVStuH4EuFHQzu8T+UQLE58ndyEeLtPt6XK0/1rxz7T5rmky75d89gvZE8X7gv/+L/L8LkvXu2eY+kpd7/G+4duwLOtSRSkegyi0aBGteFfoszwVpb3wp8j6Stkvb7sSQIcwS0/JIwqpr0edScApeyEIuzFVw6/ToLXzxRqjkSp3o7Z+/fLGKILWnFQbU8//r9djgEp9SNMAI=" + } + ], + "Route multisig/createSigningPackage should verify commitment consistency of signer set": [ + { + "version": 4, + "id": "9357293c-68e6-4c29-9968-0922f478a210", + "name": "test", + "spendingKey": "4618b19e7337351a1b1fb08da52f817887d5a9e18a336d0dc7b6be5bc48d46f9", + "viewKey": "b4b4cb192a730e0dcb5535ac158e35f62977dc007f19ff13836c42e0114ced958e53a69abb1da99167ddee884706476e873e5a04a610c8d007ea94138e04514a", + "incomingViewKey": "8baa8ebcff5517769a52bcb84402d88a838b2142ea4fffb0b479afc76f9c9f07", + "outgoingViewKey": "79ba62760c699242b85daba33b96a54095bea04c5c8045551883e1afd2c6adda", + "publicAddress": "1c2102bcc1bd0b858f31dc586e4ea4086a7811f8dd764b6af2704fcbd7fa368e", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "proofAuthorizingKey": "bf5a357446888cb624475d7a2aea6b8c3a08a3933c884bcd486310fa84158608" + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86", + "noteCommitment": { + "type": "Buffer", + "data": "base64:UCVw6UmLkCp9pbhvcCklK+guwyINWPeO+uuu9kq6YmU=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:zLnZjKE6YqflTGr7scM4QSNK7qkcS+p8/JaPzuO2Occ=" + }, + "target": "9282972777491357380673661573939192202192629606981189395159182914949423", + "randomness": "0", + "timestamp": 1710203903331, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAD8u94pZlUKCKUb3D7gO/9EQRYmwQ3h8r3o+e175jbdmK9gvWcnOaPiVFuvKGbwZmkMxS4zyoVOBMFA1CrU9Anco6e7HQE/1Zrw+kcfj8tCOLzuQLtiMslAMWUcEmEYKoV2I58uGnQtmYn4MORY+pd2sWC9oNyNkZFe+3oU58okEJkmmtbdKApC0hRkvSK6XUOX3g4YMFh6k+caeHzMb13+NwniHgZCfKeqDd/nDIh4CyZRSCnmZaJtOzqZGwJM7pjy35uzwl9XIhGcvQAaqYTFrM06idEfq6bkNHxNIjU2hP5hWvXMOejHR1iZymdgktpeoWlxam1UgjBWFGg1CnqLhxjtuRM5lYl+Zh4aGzVVbhWrBErxkZolTfTT9F+TEH2An76oREu2FkChQT/5DwculRTG/zZj5sJ+MK8UiSqcQxyIxtu+9ETlNEVfXY5+q75q00KAvw0XxD1VDAPSCIt5NbTCNI66Y+umCZDemx3EduKhmcMEtSpd1tlmmQZNMAt4Mj/uOZ/M8lKFeKQ8TXwS9wSUaVwIdCBDypr/+hBplRwAVr1bSSEBgHQVi4E2JBes1BZocE3zLtFRSpB6tLoE1BK+o1EG1SBJqT0kUfowOv8Tz3CK4Bx0lyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwYuE4RTjMR2F/ied3J5hDzENPh9dMXga/WRCbKpdsvcun8Q14GCceL09C0tiU3k0rC0q1BrVRCzQZctfpwJNkBw==" + } + ] + }, + { + "type": "Buffer", + "data": "base64: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" + } + ], + "Route multisig/createSigningPackage should verify minimum number of commitments": [ + { + "version": 4, + "id": "c28fbf97-6fcd-4a4a-99ae-78ce5b20fcd8", + "name": "test", + "spendingKey": "f58a8a98787be63b6a40d190515100b84b03c24902f35f3aa7e5945108dd7f76", + "viewKey": "6edb84efe0477aa4aeaa350b1f65a22032f3ca01a794218603ce377b82bf3e9d513de2711dba838e409c7af8e6d62dfeeb32192ca30a54c739208f9a5948c868", + "incomingViewKey": "3cd75d5214192a0588a91766c9932c158f2ab1bdefbfb5a2b2e4a776c9429b05", + "outgoingViewKey": "5f1d973663182b94654fc6ccb71960e13505b906347e4419c031465a66342320", + "publicAddress": "8644467e5da7915507b322b4f321fb0391c422936a15d131af917b9386ff7db9", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "proofAuthorizingKey": "20ec6d1bee0cca93b002b2518940c5ed2ffd188fc1ed8776b0a2cd8ff7b47008" + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86", + "noteCommitment": { + "type": "Buffer", + "data": "base64:4fFdIZ+besfS9K/GkPJIPVmiBl6YkPwu3YCJA95yRSI=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:h8/IU4gN4DmuYS6ncyxf4CkT8SIrUZwRFiugDHaYl4k=" + }, + "target": "9282972777491357380673661573939192202192629606981189395159182914949423", + "randomness": "0", + "timestamp": 1710283295434, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAA7hWH1dvXVn7KQ5tR3l5BW5ej4CUiSvzQsiZMt/5BGuiSafSuUHGe3Vx7Se6gc9IeuJCU1V4eEbMMNHN5kYf8s/5kKYh9rtdleBMzNMAoGQGwj6ffsoJyS5h8uZSod4qiIYMiQt8lVPHJ/2alNfnimBw1leefnd1IT17Dujavb34HsTrCvFHUsFry8W42wmo7Oo1bSN2QFixsrobN7QUDHyMECDKqD/v4b8fFcJpohaGCvWJg6syBwGU/J1+OIsU+IbrDxxT8gZGj2QcNNEsMhh2qkrxSNbWijoj1JrnmH76TAdZhmN/h52r6P2JK89YPQRRcSWlrATCxZFe8H2fiDCf5LWK0L389v0hsUqLZg7mICTQftoqyIPOlJPb7xPQcdlpq51ukLOn5MlkNxXCkX9B8Ir7yGQ0b24hvBf/amwb/um4Syc388c5voEgLKZmHtBXGQ/k9I2eFqOUspLE1GVvi7tGwTu9ai6lIxWbL822L0EISI5WTfhW38LgV4uE6g0F1RYcDXDKZ71CqqRQedGci9M0s3g32XAu9yDQHusggspz3ZUdADrKJfDr1SnE14NrlVcWYSxX6PDGfVtOsVuTvx/YFvlR1YzFKjYcyah/3V+z8JO73iElyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNMT0zJBXQYRsp/f6Q6Qg1CLh0D9o451NKSnNJ1YH1D4cuv621sdyTcbpbpa9ZRhD78e9Hqr+vy8TkNzhoSSbBg==" + } + ] + }, + { + "type": "Buffer", + "data": "base64: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" } ] } \ No newline at end of file diff --git a/ironfish/src/rpc/routes/wallet/multisig/aggregateSignatureShares.ts b/ironfish/src/rpc/routes/wallet/multisig/aggregateSignatureShares.ts index 7016969c40..e05d740f56 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/aggregateSignatureShares.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/aggregateSignatureShares.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 { aggregateSignatureShares } from '@ironfish/rust-nodejs' +import { multisig } from '@ironfish/rust-nodejs' import * as yup from 'yup' import { Transaction } from '../../../../primitives/transaction' import { AssertMultisig } from '../../../../wallet' @@ -50,7 +50,7 @@ routes.register => { AssertHasRpcContext(request, context, 'wallet') - const identity = await context.wallet.createMultisigSecret(request.data.name) - request.end({ identity: identity.toString('hex') }) + try { + const identity = await context.wallet.createMultisigSecret(request.data.name) + request.end({ identity: identity.toString('hex') }) + } catch (e) { + if ( + e instanceof DuplicateAccountNameError || + e instanceof DuplicateMultisigSecretNameError + ) { + throw new RpcValidationError(e.message, 400, RPC_ERROR_CODES.DUPLICATE_ACCOUNT_NAME) + } + } }, ) diff --git a/ironfish/src/rpc/routes/wallet/multisig/createSignatureShare.test.ts b/ironfish/src/rpc/routes/wallet/multisig/createSignatureShare.test.ts index 7b9633af85..d9917fa587 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/createSignatureShare.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/createSignatureShare.test.ts @@ -2,6 +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 { generateKey } from '@ironfish/rust-nodejs' +import { Assert } from '../../../../assert' +import { useAccountAndAddFundsFixture, useUnsignedTxFixture } from '../../../../testUtilities' import { createRouteTest } from '../../../../testUtilities/routeTest' import { ACCOUNT_SCHEMA_VERSION } from '../../../../wallet' @@ -47,4 +49,87 @@ describe('Route wallt/multisig/createSignatureShare', () => { }), ) }) + + it('should fail if signing package contains commitments from unknown signers', async () => { + // Create a bunch of multisig identities + const accountNames = Array.from({ length: 3 }, (_, index) => `test-account-${index}`) + const participants = await Promise.all( + accountNames.map(async (name) => { + const identity = (await routeTest.client.wallet.multisig.createParticipant({ name })) + .content.identity + return { name, identity } + }), + ) + + // Initialize the group though TDK and import the accounts generated + const trustedDealerPackage = ( + await routeTest.client.wallet.multisig.createTrustedDealerKeyPackage({ + minSigners: 2, + participants, + }) + ).content + for (const { name, identity } of participants) { + const importAccount = trustedDealerPackage.participantAccounts.find( + (account) => account.identity === identity, + ) + expect(importAccount).not.toBeUndefined() + await routeTest.client.wallet.importAccount({ + name, + account: importAccount!.account, + }) + } + + // Create an unsigned transaction + const txAccount = await useAccountAndAddFundsFixture(routeTest.wallet, routeTest.chain) + const unsignedTransaction = ( + await useUnsignedTxFixture(routeTest.wallet, txAccount, txAccount) + ) + .serialize() + .toString('hex') + + // Create signing commitments for all participants + const commitments = await Promise.all( + accountNames.map(async (accountName) => { + const signingCommitment = + await routeTest.client.wallet.multisig.createSigningCommitment({ + account: accountName, + unsignedTransaction, + signers: participants, + }) + return signingCommitment.content.commitment + }), + ) + + // Create the signing package + const signingPackage = ( + await routeTest.client.wallet.multisig.createSigningPackage({ + commitments, + unsignedTransaction, + }) + ).content.signingPackage + + // Remove one participant from the participants store to simulate unknown signer + const account = routeTest.wallet.getAccountByName(accountNames[0]) + Assert.isNotNull(account) + + await routeTest.wallet.walletDb.deleteParticipantIdentity( + account, + Buffer.from(participants[1].identity, 'hex'), + ) + + // Attempt to create signature share + await expect( + routeTest.client.wallet.multisig.createSignatureShare({ + account: account.name, + signingPackage, + }), + ).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining( + 'Signing package contains commitment from unknown signer', + ), + status: 400, + }), + ) + }) }) diff --git a/ironfish/src/rpc/routes/wallet/multisig/createSignatureShare.ts b/ironfish/src/rpc/routes/wallet/multisig/createSignatureShare.ts index 5dd3d4bdf1..5e7db581d8 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/createSignatureShare.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/createSignatureShare.ts @@ -1,9 +1,12 @@ /* 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 { createSignatureShare } from '@ironfish/rust-nodejs' +import { multisig } from '@ironfish/rust-nodejs' +import { BufferSet } from 'buffer-map' import * as yup from 'yup' +import { AsyncUtils } from '../../../../utils' import { AssertMultisigSigner } from '../../../../wallet' +import { RpcValidationError } from '../../../adapters' import { ApiNamespace } from '../../namespaces' import { routes } from '../../router' import { AssertHasRpcContext } from '../../rpcContext' @@ -36,13 +39,29 @@ export const CreateSignatureShareResponseSchema: yup.ObjectSchema( `${ApiNamespace.wallet}/multisig/createSignatureShare`, CreateSignatureShareRequestSchema, - (request, node): void => { + async (request, node): Promise => { AssertHasRpcContext(request, node, 'wallet') const account = getAccount(node.wallet, request.data.account) AssertMultisigSigner(account) - const signatureShare = createSignatureShare( + const signingPackage = new multisig.SigningPackage( + Buffer.from(request.data.signingPackage, 'hex'), + ) + + const participantIdentities = new BufferSet( + await AsyncUtils.materialize(node.wallet.walletDb.getParticipantIdentities(account)), + ) + + for (const signer of signingPackage.signers()) { + if (!participantIdentities.has(signer)) { + throw new RpcValidationError( + `Signing package contains commitment from unknown signer ${signer.toString('hex')}`, + ) + } + } + + const signatureShare = multisig.createSignatureShare( account.multisigKeys.secret, account.multisigKeys.keyPackage, request.data.signingPackage, diff --git a/ironfish/src/rpc/routes/wallet/multisig/createSigningCommitment.test.ts b/ironfish/src/rpc/routes/wallet/multisig/createSigningCommitment.test.ts index 81280b6152..040287cce7 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/createSigningCommitment.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/createSigningCommitment.test.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 { ParticipantSecret } from '@ironfish/rust-nodejs' +import { multisig } from '@ironfish/rust-nodejs' import { useAccountAndAddFundsFixture, useUnsignedTxFixture } from '../../../../testUtilities' import { createRouteTest } from '../../../../testUtilities/routeTest' import { ACCOUNT_SCHEMA_VERSION } from '../../../../wallet' @@ -31,7 +31,7 @@ describe('Route wallet/multisig/createSigningCommitment', () => { it('cannot perform signing commitment if the account is a trusted dealer', async () => { const participants = Array.from({ length: 3 }, () => ({ - identity: ParticipantSecret.random().toIdentity().serialize().toString('hex'), + identity: multisig.ParticipantSecret.random().toIdentity().serialize().toString('hex'), })) const request = { minSigners: 2, participants } diff --git a/ironfish/src/rpc/routes/wallet/multisig/createSigningCommitment.ts b/ironfish/src/rpc/routes/wallet/multisig/createSigningCommitment.ts index cd8f70888d..5e79455958 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/createSigningCommitment.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/createSigningCommitment.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 { createSigningCommitment, UnsignedTransaction } from '@ironfish/rust-nodejs' +import { multisig, UnsignedTransaction } from '@ironfish/rust-nodejs' import * as yup from 'yup' import { AssertMultisigSigner } from '../../../../wallet/account/account' import { ApiNamespace } from '../../namespaces' @@ -55,11 +55,21 @@ routes.register signer.identity) + + // always include account's own identity. ironfish-frost deduplicates identities + const accountSecret = new multisig.ParticipantSecret( + Buffer.from(account.multisigKeys.secret, 'hex'), + ) + const accountIdentity = accountSecret.toIdentity().serialize().toString('hex') + signers.push(accountIdentity) + + const commitment = multisig.createSigningCommitment( account.multisigKeys.secret, account.multisigKeys.keyPackage, unsigned.hash(), - request.data.signers.map((signer) => signer.identity), + signers, ) request.end({ commitment }) diff --git a/ironfish/src/rpc/routes/wallet/multisig/createSigningPackage.test.ts b/ironfish/src/rpc/routes/wallet/multisig/createSigningPackage.test.ts index 3c10f280fb..a68071a32c 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/createSigningPackage.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/createSigningPackage.test.ts @@ -3,7 +3,6 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { useAccountAndAddFundsFixture, useUnsignedTxFixture } from '../../../../testUtilities' import { createRouteTest } from '../../../../testUtilities/routeTest' -import { RpcRequestError } from '../../../clients' import { CreateParticipantResponse } from './createParticipant' describe('Route multisig/createSigningPackage', () => { @@ -72,51 +71,291 @@ describe('Route multisig/createSigningPackage', () => { }) }) - it('should verify commitment identities', async () => { + describe('should verify commitment consistency', () => { + it('of identities', async () => { + // Create a bunch of multisig identities + const accountNames = Array.from({ length: 4 }, (_, index) => `test-account-${index}`) + const participants = await Promise.all( + accountNames.map(async (name) => { + const identity = ( + await routeTest.client + .request('wallet/multisig/createParticipant', { name }) + .waitForEnd() + ).content.identity + return { name, identity } + }), + ) + + // Split the participants in two groups + const participantsGroup1 = participants.slice(0, 2) + const participantsGroup2 = participants.slice(2) + + // Initialize the first group though TDK and import the accounts generated + const trustedDealerPackage1 = ( + await routeTest.client.wallet.multisig.createTrustedDealerKeyPackage({ + minSigners: 2, + participants: participantsGroup1, + }) + ).content + for (const { name, identity } of participantsGroup1) { + const importAccount = trustedDealerPackage1.participantAccounts.find( + (account) => account.identity === identity, + ) + expect(importAccount).not.toBeUndefined() + await routeTest.client.wallet.importAccount({ + name, + account: importAccount!.account, + }) + } + + // Initialize the second group though TDK and import the accounts generated + const trustedDealerPackage2 = ( + await routeTest.client.wallet.multisig.createTrustedDealerKeyPackage({ + minSigners: 2, + participants: participantsGroup2, + }) + ).content + for (const { name, identity } of participantsGroup2) { + const importAccount = trustedDealerPackage2.participantAccounts.find( + (account) => account.identity === identity, + ) + expect(importAccount).not.toBeUndefined() + await routeTest.client.wallet.importAccount({ + name, + account: importAccount!.account, + }) + } + + // Create an unsigned transaction + const txAccount = await useAccountAndAddFundsFixture(routeTest.wallet, routeTest.chain) + const unsignedTransaction = ( + await useUnsignedTxFixture(routeTest.wallet, txAccount, txAccount) + ) + .serialize() + .toString('hex') + + // Create signing commitments mixing participants from different groups + const mixedParticipants = [participantsGroup1[0], participantsGroup2[0]] + const commitments = await Promise.all( + mixedParticipants.map(async ({ name }) => { + const signingCommitment = + await routeTest.client.wallet.multisig.createSigningCommitment({ + account: name, + unsignedTransaction, + signers: participants, + }) + return signingCommitment.content.commitment + }), + ) + + // Try to create the signing package + await expect(async () => + routeTest.client.wallet.multisig.createSigningPackage({ + account: mixedParticipants[0].name, + commitments, + unsignedTransaction, + }), + ).rejects.toThrow( + expect.objectContaining({ + message: expect.stringMatching( + /Commitment 1 is from identity .+, which is not part of the multsig group for account .+/, + ), + status: 400, + }), + ) + + await expect(async () => + routeTest.client.wallet.multisig.createSigningPackage({ + account: mixedParticipants[1].name, + commitments, + unsignedTransaction, + }), + ).rejects.toThrow( + expect.objectContaining({ + message: expect.stringMatching( + /Commitment 0 is from identity .+, which is not part of the multsig group for account .+/, + ), + status: 400, + }), + ) + }) + + it('of transactions', async () => { + // Create a bunch of multisig identities + const accountNames = Array.from({ length: 4 }, (_, index) => `test-account-${index}`) + const participants = await Promise.all( + accountNames.map(async (name) => { + const identity = ( + await routeTest.client + .request('wallet/multisig/createParticipant', { name }) + .waitForEnd() + ).content.identity + return { name, identity } + }), + ) + + // Initialize the group though TDK and import the accounts generated + const trustedDealerPackage = ( + await routeTest.client.wallet.multisig.createTrustedDealerKeyPackage({ + minSigners: 2, + participants, + }) + ).content + for (const { name, identity } of participants) { + const importAccount = trustedDealerPackage.participantAccounts.find( + (account) => account.identity === identity, + ) + expect(importAccount).not.toBeUndefined() + await routeTest.client.wallet.importAccount({ + name, + account: importAccount!.account, + }) + } + + // Create two unsigned transactions + const txAccount = await useAccountAndAddFundsFixture(routeTest.wallet, routeTest.chain) + const unsignedTransaction1 = ( + await useUnsignedTxFixture(routeTest.wallet, txAccount, txAccount) + ) + .serialize() + .toString('hex') + const unsignedTransaction2 = ( + await useUnsignedTxFixture(routeTest.wallet, txAccount, txAccount) + ) + .serialize() + .toString('hex') + + // Create signing commitments mixing participants from different groups + const commitments = [ + ( + await routeTest.client.wallet.multisig.createSigningCommitment({ + account: participants[0].name, + unsignedTransaction: unsignedTransaction1, + signers: participants, + }) + ).content.commitment, + ( + await routeTest.client.wallet.multisig.createSigningCommitment({ + account: participants[1].name, + unsignedTransaction: unsignedTransaction2, + signers: participants, + }) + ).content.commitment, + ] + + // Try to create the signing package + await expect(async () => + routeTest.client.wallet.multisig.createSigningPackage({ + account: participants[0].name, + commitments, + unsignedTransaction: unsignedTransaction1, + }), + ).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining( + 'Commitment 1 was not generated for the given unsigned transaction and signer set', + ), + status: 400, + }), + ) + }) + + it('of signer set', async () => { + // Create a bunch of multisig identities + const accountNames = Array.from({ length: 4 }, (_, index) => `test-account-${index}`) + const participants = await Promise.all( + accountNames.map(async (name) => { + const identity = ( + await routeTest.client + .request('wallet/multisig/createParticipant', { name }) + .waitForEnd() + ).content.identity + return { name, identity } + }), + ) + + // Initialize the group though TDK and import the accounts generated + const trustedDealerPackage = ( + await routeTest.client.wallet.multisig.createTrustedDealerKeyPackage({ + minSigners: 2, + participants, + }) + ).content + for (const { name, identity } of participants) { + const importAccount = trustedDealerPackage.participantAccounts.find( + (account) => account.identity === identity, + ) + expect(importAccount).not.toBeUndefined() + await routeTest.client.wallet.importAccount({ + name, + account: importAccount!.account, + }) + } + + // Create an unsigned transactions + const txAccount = await useAccountAndAddFundsFixture(routeTest.wallet, routeTest.chain) + const unsignedTransaction = ( + await useUnsignedTxFixture(routeTest.wallet, txAccount, txAccount) + ) + .serialize() + .toString('hex') + + // Create signing commitments mixing participants from different groups + const commitments = [ + ( + await routeTest.client.wallet.multisig.createSigningCommitment({ + account: participants[0].name, + unsignedTransaction, + signers: participants, + }) + ).content.commitment, + ( + await routeTest.client.wallet.multisig.createSigningCommitment({ + account: participants[1].name, + unsignedTransaction, + signers: [participants[0], participants[1]], + }) + ).content.commitment, + ] + + // Try to create the signing package + await expect(async () => + routeTest.client.wallet.multisig.createSigningPackage({ + account: participants[0].name, + commitments, + unsignedTransaction, + }), + ).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining( + 'Commitment 1 was not generated for the given unsigned transaction and signer set', + ), + status: 400, + }), + ) + }) + }) + + it('should verify minimum number of commitments', async () => { // Create a bunch of multisig identities - const accountNames = Array.from({ length: 4 }, (_, index) => `test-account-${index}`) + const accountNames = Array.from({ length: 3 }, (_, index) => `test-account-${index}`) const participants = await Promise.all( accountNames.map(async (name) => { - const identity = ( - await routeTest.client - .request('wallet/multisig/createParticipant', { name }) - .waitForEnd() - ).content.identity + const identity = (await routeTest.client.wallet.multisig.createParticipant({ name })) + .content.identity return { name, identity } }), ) - // Split the participants in two groups - const participantsGroup1 = participants.slice(0, 2) - const participantsGroup2 = participants.slice(2) - - // Initialize the first group though TDK and import the accounts generated - const trustedDealerPackage1 = ( - await routeTest.client.wallet.multisig.createTrustedDealerKeyPackage({ - minSigners: 2, - participants: participantsGroup1, - }) - ).content - for (const { name, identity } of participantsGroup1) { - const importAccount = trustedDealerPackage1.participantAccounts.find( - (account) => account.identity === identity, - ) - expect(importAccount).not.toBeUndefined() - await routeTest.client.wallet.importAccount({ - name, - account: importAccount!.account, - }) - } - - // Initialize the second group though TDK and import the accounts generated - const trustedDealerPackage2 = ( + // Initialize the group though TDK and import the accounts generated + const trustedDealerPackage = ( await routeTest.client.wallet.multisig.createTrustedDealerKeyPackage({ minSigners: 2, - participants: participantsGroup2, + participants, }) ).content - for (const { name, identity } of participantsGroup2) { - const importAccount = trustedDealerPackage2.participantAccounts.find( + for (const { name, identity } of participants) { + const importAccount = trustedDealerPackage.participantAccounts.find( (account) => account.identity === identity, ) expect(importAccount).not.toBeUndefined() @@ -134,35 +373,28 @@ describe('Route multisig/createSigningPackage', () => { .serialize() .toString('hex') - // Create signing commitments mixing participants from different groups - const mixedParticipants = [participantsGroup1[0], participantsGroup2[0]] - const commitments = await Promise.all( - mixedParticipants.map(async ({ name }) => { - const signingCommitment = - await routeTest.client.wallet.multisig.createSigningCommitment({ - account: name, - unsignedTransaction, - signers: participants, - }) - return signingCommitment.content.commitment - }), - ) - - // Try to create the signing package - await expect(async () => - routeTest.client.wallet.multisig.createSigningPackage({ - account: mixedParticipants[0].name, - commitments, + // Create only one signing commitment + const signingCommitment = ( + await routeTest.client.wallet.multisig.createSigningCommitment({ + account: accountNames[0], unsignedTransaction, - }), - ).rejects.toThrow(RpcRequestError) + signers: participants, + }) + ).content.commitment await expect(async () => routeTest.client.wallet.multisig.createSigningPackage({ - account: mixedParticipants[1].name, - commitments, + account: accountNames[0], + commitments: [signingCommitment], unsignedTransaction, }), - ).rejects.toThrow(RpcRequestError) + ).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining( + 'A minimum of 2 signers is required for a valid signature. Only 1 commitments provided', + ), + status: 400, + }), + ) }) }) diff --git a/ironfish/src/rpc/routes/wallet/multisig/createSigningPackage.ts b/ironfish/src/rpc/routes/wallet/multisig/createSigningPackage.ts index eadf32f0bd..c0aba68427 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/createSigningPackage.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/createSigningPackage.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 { PublicKeyPackage, SigningCommitment, UnsignedTransaction } from '@ironfish/rust-nodejs' +import { multisig, UnsignedTransaction } from '@ironfish/rust-nodejs' import * as yup from 'yup' import { AssertMultisig } from '../../../../wallet' import { RpcValidationError } from '../../../adapters' @@ -45,25 +45,51 @@ routes.register identity.toString('hex')), + const publicKeyPackage = new multisig.PublicKeyPackage( + account.multisigKeys.publicKeyPackage, ) + const identitySet = publicKeyPackage + .identities() + .map((identity) => identity.toString('hex')) - for (const commitment of request.data.commitments) { - const signingCommitment = new SigningCommitment(Buffer.from(commitment, 'hex')) - const identity = signingCommitment.identity().toString('hex') - if (!identitySet.has(identity)) { + if (request.data.commitments.length < publicKeyPackage.minSigners()) { + throw new RpcValidationError( + `A minimum of ${publicKeyPackage.minSigners()} signers is required for a valid signature. Only ${ + request.data.commitments.length + } commitments provided`, + 400, + ) + } + + const commitments = request.data.commitments.map( + (commitment) => new multisig.SigningCommitment(Buffer.from(commitment, 'hex')), + ) + + // Verify the consistency of commitments. Loop twice because the first loop + // gives a more specific error message (easier to debug) + for (const [index, commitment] of commitments.entries()) { + const identity = commitment.identity().toString('hex') + if (!identitySet.includes(identity)) { + throw new RpcValidationError( + `Commitment ${index} is from identity ${identity}, which is not part of the multsig group for account ${account.name}`, + 400, + ) + } + } + for (const [index, commitment] of commitments.entries()) { + if (!commitment.verifyChecksum(transactionHash, identitySet)) { throw new RpcValidationError( - `Received commitment from identity (${identity}) that is not part of the multsig group for account ${account.name}`, + `Commitment ${index} was not generated for the given unsigned transaction and signer set`, 400, ) } } + const signingPackage = unsignedTransaction.signingPackage(request.data.commitments) request.end({ diff --git a/ironfish/src/rpc/routes/wallet/multisig/createTrustedDealerKeyPackage.test.ts b/ironfish/src/rpc/routes/wallet/multisig/createTrustedDealerKeyPackage.test.ts index 5e1ab54016..5f11952eb7 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/createTrustedDealerKeyPackage.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/createTrustedDealerKeyPackage.test.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 { ParticipantSecret } from '@ironfish/rust-nodejs' +import { multisig } from '@ironfish/rust-nodejs' import { createRouteTest } from '../../../../testUtilities/routeTest' describe('Route multisig/createTrustedDealerKeyPackage', () => { @@ -9,7 +9,7 @@ describe('Route multisig/createTrustedDealerKeyPackage', () => { it('should create trusted dealer key package', async () => { const participants = Array.from({ length: 3 }, () => ({ - identity: ParticipantSecret.random().toIdentity().serialize().toString('hex'), + identity: multisig.ParticipantSecret.random().toIdentity().serialize().toString('hex'), })) const request = { minSigners: 2, participants } const response = await routeTest.client diff --git a/ironfish/src/rpc/routes/wallet/multisig/createTrustedDealerKeyPackage.ts b/ironfish/src/rpc/routes/wallet/multisig/createTrustedDealerKeyPackage.ts index 8c3bc3f529..b91fd755f8 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/createTrustedDealerKeyPackage.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/createTrustedDealerKeyPackage.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 { generateAndSplitKey } from '@ironfish/rust-nodejs' +import { multisig } from '@ironfish/rust-nodejs' import * as yup from 'yup' import { Assert } from '../../../../assert' import { FullNode } from '../../../../node' @@ -76,7 +76,7 @@ routes.register< outgoingViewKey, proofAuthorizingKey, keyPackages, - } = generateAndSplitKey(minSigners, identities) + } = multisig.generateAndSplitKey(minSigners, identities) const latestHeader = node.chain.latest const createdAt = { diff --git a/ironfish/src/rpc/routes/wallet/multisig/getAccountIdentities.ts b/ironfish/src/rpc/routes/wallet/multisig/getAccountIdentities.ts index 9b23b4d13b..2859795557 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/getAccountIdentities.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/getAccountIdentities.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 { PublicKeyPackage } from '@ironfish/rust-nodejs' +import { multisig } from '@ironfish/rust-nodejs' import * as yup from 'yup' import { AssertMultisig } from '../../../../wallet' import { ApiNamespace } from '../../namespaces' @@ -39,7 +39,9 @@ routes.register identity.toString('hex')) request.end({ identities }) diff --git a/ironfish/src/rpc/routes/wallet/multisig/getIdentities.ts b/ironfish/src/rpc/routes/wallet/multisig/getIdentities.ts new file mode 100644 index 0000000000..ea7ba49f94 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/multisig/getIdentities.ts @@ -0,0 +1,57 @@ +/* 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 { ApiNamespace } from '../../namespaces' +import { routes } from '../../router' +import { AssertHasRpcContext } from '../../rpcContext' + +export type GetIdentitiesRequest = Record | undefined + +export type GetIdentitiesResponse = { + identities: Array<{ + name: string + identity: string + }> +} + +export const GetIdentitiesRequestSchema: yup.MixedSchema = yup + .mixed() + .oneOf([undefined] as const) + +export const GetIdentitiesResponseSchema: yup.ObjectSchema = yup + .object({ + identities: yup + .array( + yup + .object({ + name: yup.string().defined(), + identity: yup.string().defined(), + }) + .defined(), + ) + .defined(), + }) + .defined() + +routes.register( + `${ApiNamespace.wallet}/multisig/getIdentities`, + GetIdentitiesRequestSchema, + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') + + const identities = [] + + for await (const [ + identity, + { name }, + ] of context.wallet.walletDb.multisigSecrets.getAllIter()) { + identities.push({ + name, + identity: identity.toString('hex'), + }) + } + + request.end({ identities }) + }, +) diff --git a/ironfish/src/rpc/routes/wallet/multisig/getIdentity.ts b/ironfish/src/rpc/routes/wallet/multisig/getIdentity.ts index 5107e84afb..59c0d16325 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/getIdentity.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/getIdentity.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 { ParticipantSecret } from '@ironfish/rust-nodejs' +import { multisig } from '@ironfish/rust-nodejs' import * as yup from 'yup' import { RpcValidationError } from '../../../adapters/errors' import { ApiNamespace } from '../../namespaces' @@ -41,7 +41,7 @@ routes.register( throw new RpcValidationError(`No identity found with name ${name}`, 404) } - const secret = new ParticipantSecret(record.secret) + const secret = new multisig.ParticipantSecret(record.secret) const identity = secret.toIdentity() request.end({ identity: identity.serialize().toString('hex') }) diff --git a/ironfish/src/rpc/routes/wallet/multisig/index.ts b/ironfish/src/rpc/routes/wallet/multisig/index.ts index 3a2911f7ad..d0a65e191b 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/index.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/index.ts @@ -9,4 +9,5 @@ export * from './createTrustedDealerKeyPackage' export * from './createSignatureShare' export * from './createParticipant' export * from './getIdentity' +export * from './getIdentities' export * from './getAccountIdentities' diff --git a/ironfish/src/rpc/routes/wallet/multisig/integration.test.slow.ts b/ironfish/src/rpc/routes/wallet/multisig/integration.test.slow.ts index d681f3bd60..ddc99bd32e 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/integration.test.slow.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/integration.test.slow.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 { Asset, ParticipantSecret, verifyTransactions } from '@ironfish/rust-nodejs' +import { Asset, multisig, verifyTransactions } from '@ironfish/rust-nodejs' import { Assert } from '../../../../assert' import { createRouteTest } from '../../../../testUtilities/routeTest' import { Account, ACCOUNT_SCHEMA_VERSION, AssertMultisigSigner } from '../../../../wallet' @@ -69,7 +69,9 @@ describe('multisig RPC integration', () => { // build list of signers const signers = participantAccounts.map((participant) => { AssertMultisigSigner(participant) - const secret = new ParticipantSecret(Buffer.from(participant.multisigKeys.secret, 'hex')) + const secret = new multisig.ParticipantSecret( + Buffer.from(participant.multisigKeys.secret, 'hex'), + ) return { identity: secret.toIdentity().serialize().toString('hex') } }) diff --git a/ironfish/src/rpc/routes/wallet/utils.ts b/ironfish/src/rpc/routes/wallet/utils.ts index 10168a8c58..e4c19c1e6b 100644 --- a/ironfish/src/rpc/routes/wallet/utils.ts +++ b/ironfish/src/rpc/routes/wallet/utils.ts @@ -4,7 +4,7 @@ import { Config } from '../../../fileStores' import { Note } from '../../../primitives' import { BufferUtils, CurrencyUtils } from '../../../utils' -import { Account, Wallet } from '../../../wallet' +import { Account, Base64JsonEncoder, Wallet } from '../../../wallet' import { isMultisigSignerImport, isMultisigSignerTrustedDealerImport, @@ -279,3 +279,20 @@ export async function serializeRpcAccountStatus( viewOnly: !account.isSpendingAccount(), } } + +export async function tryDecodeAccountWithMultisigSecrets( + wallet: Wallet, + value: string, +): Promise { + const encoder = new Base64JsonEncoder() + + for await (const { name, secret } of wallet.walletDb.getMultisigSecrets()) { + try { + return encoder.decode(value, { name, multisigSecret: secret }) + } catch (e: unknown) { + continue + } + } + + return undefined +} diff --git a/ironfish/src/testUtilities/keys.ts b/ironfish/src/testUtilities/keys.ts index 33d95ad704..1677cc5efb 100644 --- a/ironfish/src/testUtilities/keys.ts +++ b/ironfish/src/testUtilities/keys.ts @@ -1,19 +1,14 @@ /* 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 { - generateAndSplitKey, - ParticipantSecret, - TrustedDealerKeyPackages, -} from '@ironfish/rust-nodejs' +import { multisig } from '@ironfish/rust-nodejs' export function createTrustedDealerKeyPackages( minSigners: number = 2, maxSigners: number = 2, -): TrustedDealerKeyPackages { +): multisig.TrustedDealerKeyPackages { const identities = Array.from({ length: maxSigners }, () => - ParticipantSecret.random().toIdentity().serialize().toString('hex'), + multisig.ParticipantSecret.random().toIdentity().serialize().toString('hex'), ) - return generateAndSplitKey(minSigners, identities) + return multisig.generateAndSplitKey(minSigners, identities) } diff --git a/ironfish/src/wallet/account/encoder/base64json.test.ts b/ironfish/src/wallet/account/encoder/base64json.test.ts index b0de6c5c21..1f604fbb1a 100644 --- a/ironfish/src/wallet/account/encoder/base64json.test.ts +++ b/ironfish/src/wallet/account/encoder/base64json.test.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 { generateKey, ParticipantSecret } from '@ironfish/rust-nodejs' +import { generateKey, multisig } from '@ironfish/rust-nodejs' import { AccountImport } from '../../walletdb/accountValue' import { ACCOUNT_SCHEMA_VERSION } from '../account' import { @@ -184,7 +184,7 @@ describe('Base64JsonEncoder', () => { }) describe('with multisig encryption', () => { - const multisigSecret = ParticipantSecret.random() + const multisigSecret = multisig.ParticipantSecret.random() const identity = multisigSecret.toIdentity() it(`produces a base64 blob with the ${BASE64_JSON_MULTISIG_ENCRYPTED_ACCOUNT_PREFIX} prefix`, () => { @@ -266,7 +266,7 @@ describe('Base64JsonEncoder', () => { const encoded = encoder.encode(accountImport, { encryptWith: { kind: 'MultisigIdentity', identity }, }) - const wrongSecret = ParticipantSecret.random() + const wrongSecret = multisig.ParticipantSecret.random() expect(() => encoder.decode(encoded, { multisigSecret: wrongSecret })).toThrow() }) diff --git a/ironfish/src/wallet/account/encoder/encoder.ts b/ironfish/src/wallet/account/encoder/encoder.ts index 2355e74a5d..379068fc63 100644 --- a/ironfish/src/wallet/account/encoder/encoder.ts +++ b/ironfish/src/wallet/account/encoder/encoder.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 { ParticipantIdentity, ParticipantSecret } from '@ironfish/rust-nodejs' +import { multisig } from '@ironfish/rust-nodejs' import { LanguageKey } from '../../../utils' import { AccountImport } from '../../walletdb/accountValue' @@ -20,7 +20,7 @@ export class DecodeFailed extends Error { } } -export class MultisigMissingSecretName extends DecodeInvalid { +export class MultisigSecretNotFound extends DecodeInvalid { name = this.constructor.name } @@ -33,7 +33,7 @@ export enum AccountFormat { export interface MultisigIdentityEncryption { kind: 'MultisigIdentity' - identity: ParticipantIdentity | Buffer + identity: multisig.ParticipantIdentity | Buffer } // This is meant to be a tagged union type: `AccountEncryptionMethod = Method1 | Method2 | Method3 | ...` @@ -56,7 +56,7 @@ export type AccountDecodingOptions = { // encoders extract all the decryption information they needed from it, but // sadly interacting with the wallet DB is an asynchronous operation, and // decoders are all synchronous - multisigSecret?: ParticipantSecret | Buffer + multisigSecret?: multisig.ParticipantSecret | Buffer } export type AccountEncoder = { diff --git a/ironfish/src/wallet/account/encoder/multisig.ts b/ironfish/src/wallet/account/encoder/multisig.ts index 8ce1ed6370..0ca599e2df 100644 --- a/ironfish/src/wallet/account/encoder/multisig.ts +++ b/ironfish/src/wallet/account/encoder/multisig.ts @@ -1,11 +1,11 @@ /* 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 { ParticipantIdentity, ParticipantSecret } from '@ironfish/rust-nodejs' +import { multisig } from '@ironfish/rust-nodejs' import { AccountDecodingOptions, MultisigIdentityEncryption, - MultisigMissingSecretName, + MultisigSecretNotFound, } from './encoder' export function encodeEncryptedMultisigAccount( @@ -13,7 +13,7 @@ export function encodeEncryptedMultisigAccount( options: MultisigIdentityEncryption, ): Buffer { const identity = Buffer.isBuffer(options.identity) - ? new ParticipantIdentity(options.identity) + ? new multisig.ParticipantIdentity(options.identity) : options.identity return identity.encryptData(value) } @@ -23,12 +23,12 @@ export function decodeEncryptedMultisigAccount( options?: AccountDecodingOptions, ): Buffer { if (!options?.multisigSecret) { - throw new MultisigMissingSecretName( - 'Encrypted multisig account cannot be decrypted without a multisig secret', + throw new MultisigSecretNotFound( + 'Encrypted multisig account cannot be decrypted without a corresponding multisig secret', ) } const secret = Buffer.isBuffer(options.multisigSecret) - ? new ParticipantSecret(options.multisigSecret) + ? new multisig.ParticipantSecret(options.multisigSecret) : options.multisigSecret try { return secret.decryptData(value) diff --git a/ironfish/src/wallet/wallet.test.slow.ts b/ironfish/src/wallet/wallet.test.slow.ts index 4e99620f9a..b5f73e2c01 100644 --- a/ironfish/src/wallet/wallet.test.slow.ts +++ b/ironfish/src/wallet/wallet.test.slow.ts @@ -1,16 +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 { - aggregateSignatureShares, - Asset, - ASSET_ID_LENGTH, - createSignatureShare, - createSigningCommitment, - generateAndSplitKey, - generateKey, - ParticipantSecret, -} from '@ironfish/rust-nodejs' +import { Asset, ASSET_ID_LENGTH, generateKey, multisig } from '@ironfish/rust-nodejs' import { Assert } from '../assert' import { Transaction } from '../primitives' import { Target } from '../primitives/target' @@ -1146,7 +1137,7 @@ describe('Wallet', () => { const accountNames = Array.from({ length: 3 }, (_, index) => `test-account-${index}`) const identities = await Promise.all( accountNames.map(async (name) => { - const secret = ParticipantSecret.random() + const secret = multisig.ParticipantSecret.random() const identity = secret.toIdentity() await node.wallet.walletDb.putMultisigSecret(identity.serialize(), { @@ -1160,7 +1151,7 @@ describe('Wallet', () => { // construct 3 separate secrets for the participants // take the secrets and get identities back (get identity first then identifier) - const trustedDealerPackage = generateAndSplitKey(minSigners, identities) + const trustedDealerPackage = multisig.generateAndSplitKey(minSigners, identities) const getMultisigKeys = (index: number) => { return { @@ -1274,7 +1265,7 @@ describe('Wallet', () => { const signers = participants.map((participant) => { AssertMultisigSigner(participant) - const secret = new ParticipantSecret( + const secret = new multisig.ParticipantSecret( Buffer.from(participant.multisigKeys.secret, 'hex'), ) return secret.toIdentity().serialize().toString('hex') @@ -1284,7 +1275,7 @@ describe('Wallet', () => { for (const participant of participants) { AssertMultisigSigner(participant) signingCommitments.push( - createSigningCommitment( + multisig.createSigningCommitment( participant.multisigKeys.secret, participant.multisigKeys.keyPackage, transactionHash, @@ -1300,7 +1291,7 @@ describe('Wallet', () => { for (const participant of participants) { AssertMultisigSigner(participant) signatureShares.push( - createSignatureShare( + multisig.createSignatureShare( participant.multisigKeys.secret, participant.multisigKeys.keyPackage, signingPackage, @@ -1309,7 +1300,7 @@ describe('Wallet', () => { } Assert.isNotUndefined(coordinator.multisigKeys) - const serializedFrostTransaction = aggregateSignatureShares( + const serializedFrostTransaction = multisig.aggregateSignatureShares( coordinator.multisigKeys.publicKeyPackage, signingPackage, signatureShares, @@ -1345,7 +1336,7 @@ describe('Wallet', () => { const accountNames = Array.from({ length: 3 }, (_, index) => `test-account-${index}`) const identities = await Promise.all( accountNames.map(async (name) => { - const secret = ParticipantSecret.random() + const secret = multisig.ParticipantSecret.random() const identity = secret.toIdentity() await node.wallet.walletDb.putMultisigSecret(identity.serialize(), { @@ -1356,7 +1347,7 @@ describe('Wallet', () => { }), ) - const trustedDealerPackage = generateAndSplitKey(minSigners, identities) + const trustedDealerPackage = multisig.generateAndSplitKey(minSigners, identities) const account = await node.wallet.importAccount({ version: 2, diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index 7f1120a564..3bf737224e 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -5,9 +5,8 @@ import { Asset, generateKey, MEMO_LENGTH, + multisig, Note as NativeNote, - ParticipantSecret, - PublicKeyPackage, UnsignedTransaction, } from '@ironfish/rust-nodejs' import { BufferMap, BufferSet } from 'buffer-map' @@ -1610,7 +1609,9 @@ export class Wallet { } if (account.multisigKeys) { - const publicKeyPackage = new PublicKeyPackage(account.multisigKeys.publicKeyPackage) + const publicKeyPackage = new multisig.PublicKeyPackage( + account.multisigKeys.publicKeyPackage, + ) for (const identity of publicKeyPackage.identities()) { await this.walletDb.addParticipantIdentity(account, identity, tx) @@ -1909,7 +1910,7 @@ export class Wallet { throw new DuplicateAccountNameError(name) } - const secret = ParticipantSecret.random() + const secret = multisig.ParticipantSecret.random() const identity = secret.toIdentity() await this.walletDb.putMultisigSecret( diff --git a/ironfish/src/wallet/walletdb/multisigSecretValue.ts b/ironfish/src/wallet/walletdb/multisigSecretValue.ts index cd4ad0a321..079d713e48 100644 --- a/ironfish/src/wallet/walletdb/multisigSecretValue.ts +++ b/ironfish/src/wallet/walletdb/multisigSecretValue.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 { SECRET_LEN } from '@ironfish/rust-nodejs' +import { multisig } from '@ironfish/rust-nodejs' import bufio from 'bufio' import { IDatabaseEncoding } from '../../storage' @@ -21,14 +21,14 @@ export class MultisigSecretValueEncoding implements IDatabaseEncoding { const walletDb = node.wallet.walletDb const name = 'test' - const secret = ParticipantSecret.random() + const secret = multisig.ParticipantSecret.random() const serializedSecret = secret.serialize() await walletDb.putMultisigSecret(secret.toIdentity().serialize(), { @@ -418,7 +418,7 @@ describe('WalletDB', () => { const account = await useAccountFixture(node.wallet, 'multisig') - const identity = ParticipantSecret.random().toIdentity() + const identity = multisig.ParticipantSecret.random().toIdentity() await walletDb.addParticipantIdentity(account, identity.serialize()) diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index 0f8c7d7d18..437c162f7b 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -1311,6 +1311,12 @@ export class WalletDB { return (await this.getMultisigSecretByName(name, tx)) !== undefined } + async *getMultisigSecrets(tx?: IDatabaseTransaction): AsyncGenerator { + for await (const value of this.multisigSecrets.getAllValuesIter(tx)) { + yield value + } + } + async addParticipantIdentity( account: Account, identity: Buffer, diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f701aa5354..624eb0ea63 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.66.1" +channel = "1.76.0"