From 2578019956b5188d68e6ea6f091a82c1a1148038 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Mon, 26 Feb 2024 13:08:17 -0800 Subject: [PATCH] wip/frost-encryption, address book, stored identities (#4775) * adds walletdb multisigSecrets (#4735) adds a datastore to the walletDb to store each 'ParticipantSecret' that a multisig user generates uses a string 'name' as the key and stores the secret as a buffer as the datastore value * adds wallet multisig rpc to create secret (#4737) * adds wallet multisig rpc to create secret provides an interface for generating a random ParticipantSecret and storing it in the walletDb with a given name updates wallet:multisig:identity:create to use the new RPC. perhaps we should rename to 'wallet:multisig:secret:create'? * changes createSecret to createIdentity returns identity instead of exposing secret over rpc * checks for duplicate names before creating identity avoids overwriting secrets in the database! reuses DUPLICATE_ACCOUNT_NAME error code, close enough prompts for new name on collision * addresses pr feedback removes extra name prompt logic removes unnecessary ParticipantIdentity construction adds database transaction to rpc * fixes typo s/assoicate/associate/ * supports retrieving identities from walletdb (#4738) adds getIdentity RPC to get an identity from the walletdb by name adds wallet:multisig:identity command to get an identity and print it on the command line * adds participant 'address book' (#4569) * adds participant 'address book' adds a database store to the walletDb that stores identifiers for participants in the signing group of a multisig account 'participantIdentifiers' uses a compound key consisting of the account prefix and a participant identifier where each participant identifier is expected to be a hex string implements methods for adding, deleting, and iterating over identifiers * uses 'pi' for participantIdentifiers datastore name * renames to participantIdentities we prefer to store Identities rather than Identifiers * adds participant identity encoder --------- Co-authored-by: Joe * fixes encoding of ParticipantIdentity on write (#4747) the database implicitly uses the encoder to serialized values at the time of writing to the database serializing with the encoder before write results in a double-encoded value, which produces a write error adds unit test to walletdb test * saves participant identities on account import (#4761) * saves participant identities on account import adds each participant identity from a PublicKeyPackage to the participantIdentities store when importing a multisig account adds napi bindings to deserialize a PublicKeyPackage and return the list of identities as buffers adds slow unit test to verify that identities are saved when importing multisig accounts * fixes test removes fake public key package from import * fixes tests adds test utility to create trusted dealer key packages for random identities replaces fake public key packages in import/export tests with generated public key packages removes bech32 test of multisig account import replaces json test of multisig account import * changes implementation of NativePublicKeyPackage.identities no napi errors to handle, fewer allocations * updates return type of identities * supports listing identities for a multisig account (#4767) * supports listing identities for a multisig account adds wallet/multisig/getAccountIdentities RPC to return a list of all identities for a given account. reads identities from the account's publicKeyPackage adds wallet:multisig:account:identities CLI command that calls the above RPC and logs each identity * removes unnecessary 'required: false' for account flag defaults to false * verify that commitments all come from known identities (#4748) * verify that commitments all come from known identities adds account parameter to wallet/multisig/createSigningPackage updates cli command with account flag before creating signing package, verifies that the identity on each commitment passed to createSigningPackage matches an identity in the set of all identities for the group uses getParticipantIdentities to build the set of identities adds unit test * updates cli flag description * uses identities from publicKeyPackage to verify commitments don't need to use address book just to list identities since they're all in the publicKeyPackage * regenerates outdated transaction fixture --------- Co-authored-by: Joe --- .../wallet/multisig/account/identities.ts | 33 +++++++ .../wallet/multisig/create-signing-package.ts | 6 ++ .../wallet/multisig/identity/create.ts | 41 +++++++-- .../wallet/multisig/identity/index.ts | 31 +++++++ ironfish-rust-nodejs/index.d.ts | 6 ++ ironfish-rust-nodejs/index.js | 4 +- ironfish-rust-nodejs/src/frost.rs | 34 ++++++- ironfish-rust/src/frost_utils/mod.rs | 2 + ironfish/src/rpc/clients/client.ts | 34 +++++++ .../1p20p0_bech32_multisig.txt | 1 - .../1p20p0_json_multisig.txt | 2 +- .../rpc/routes/wallet/exportAccount.test.ts | 16 ++-- .../rpc/routes/wallet/importAccount.test.ts | 13 ++- .../createSigningPackage.test.ts.fixture | 50 +++++++++++ .../routes/wallet/multisig/createIdentity.ts | 55 ++++++++++++ .../multisig/createSignatureShare.test.ts | 1 - .../multisig/createSigningPackage.test.ts | 90 ++++++++++++++++++- .../wallet/multisig/createSigningPackage.ts | 30 ++++++- .../wallet/multisig/getAccountIdentities.ts | 47 ++++++++++ .../rpc/routes/wallet/multisig/getIdentity.ts | 50 +++++++++++ .../src/rpc/routes/wallet/multisig/index.ts | 3 + ironfish/src/testUtilities/index.ts | 1 + ironfish/src/testUtilities/keys.ts | 21 +++++ ironfish/src/wallet/wallet.test.slow.ts | 39 ++++++++ ironfish/src/wallet/wallet.ts | 9 ++ .../__fixtures__/walletdb.test.ts.fixture | 20 +++++ .../walletdb/participantIdentity.test.ts | 30 +++++++ .../wallet/walletdb/participantIdentity.ts | 43 +++++++++ ironfish/src/wallet/walletdb/walletdb.test.ts | 39 +++++++- ironfish/src/wallet/walletdb/walletdb.ts | 78 ++++++++++++++++ 30 files changed, 795 insertions(+), 34 deletions(-) create mode 100644 ironfish-cli/src/commands/wallet/multisig/account/identities.ts create mode 100644 ironfish-cli/src/commands/wallet/multisig/identity/index.ts delete mode 100644 ironfish/src/rpc/routes/wallet/__importTestCases__/1p20p0_bech32_multisig.txt create mode 100644 ironfish/src/rpc/routes/wallet/multisig/createIdentity.ts create mode 100644 ironfish/src/rpc/routes/wallet/multisig/getAccountIdentities.ts create mode 100644 ironfish/src/rpc/routes/wallet/multisig/getIdentity.ts create mode 100644 ironfish/src/testUtilities/keys.ts create mode 100644 ironfish/src/wallet/walletdb/participantIdentity.test.ts create mode 100644 ironfish/src/wallet/walletdb/participantIdentity.ts diff --git a/ironfish-cli/src/commands/wallet/multisig/account/identities.ts b/ironfish-cli/src/commands/wallet/multisig/account/identities.ts new file mode 100644 index 0000000000..54ccb93029 --- /dev/null +++ b/ironfish-cli/src/commands/wallet/multisig/account/identities.ts @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { Flags } from '@oclif/core' +import { IronfishCommand } from '../../../../command' +import { RemoteFlags } from '../../../../flags' + +export class MultisigAccountIdentities extends IronfishCommand { + static description = `List the identities for a multisig account` + static hidden = true + + static flags = { + ...RemoteFlags, + account: Flags.string({ + char: 'f', + description: 'The account to list identities for', + }), + } + + async start(): Promise { + const { flags } = await this.parse(MultisigAccountIdentities) + + const client = await this.sdk.connectRpc() + + const response = await client.wallet.multisig.getAccountIdentities({ + account: flags.account, + }) + + for (const identity of response.content.identities) { + this.log(identity) + } + } +} diff --git a/ironfish-cli/src/commands/wallet/multisig/create-signing-package.ts b/ironfish-cli/src/commands/wallet/multisig/create-signing-package.ts index 7a39586f33..bc60eaa5c4 100644 --- a/ironfish-cli/src/commands/wallet/multisig/create-signing-package.ts +++ b/ironfish-cli/src/commands/wallet/multisig/create-signing-package.ts @@ -13,6 +13,11 @@ export class CreateSigningPackage extends IronfishCommand { static flags = { ...RemoteFlags, + account: Flags.string({ + char: 'f', + description: 'The account to use when creating the signing package', + required: false, + }), unsignedTransaction: Flags.string({ char: 'u', description: 'The unsigned transaction for which the signing share will be created', @@ -48,6 +53,7 @@ export class CreateSigningPackage extends IronfishCommand { const client = await this.sdk.connectRpc() const signingPackageResponse = await client.wallet.multisig.createSigningPackage({ + account: flags.account, unsignedTransaction, commitments, }) diff --git a/ironfish-cli/src/commands/wallet/multisig/identity/create.ts b/ironfish-cli/src/commands/wallet/multisig/identity/create.ts index 4bad9ff0b5..417ec96c7f 100644 --- a/ironfish-cli/src/commands/wallet/multisig/identity/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/identity/create.ts @@ -1,19 +1,48 @@ /* 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 { RPC_ERROR_CODES, RpcRequestError } from '@ironfish/sdk' +import { CliUx, Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' +import { RemoteFlags } from '../../../../flags' export class MultisigIdentityCreate extends IronfishCommand { static description = `Create a multisig identity` static hidden = true - start(): void { - // TODO: generate secret over RPC, persist in walletDb - const secret = ParticipantSecret.random() + static flags = { + ...RemoteFlags, + name: Flags.string({ + char: 'n', + description: 'Name to associate with the identity', + required: true, + }), + } + + async start(): Promise { + const { flags } = await this.parse(MultisigIdentityCreate) + + const client = await this.sdk.connectRpc() + let response + while (!response) { + try { + response = await client.wallet.multisig.createIdentity({ name: flags.name }) + } catch (e) { + if ( + e instanceof RpcRequestError && + e.code === RPC_ERROR_CODES.DUPLICATE_ACCOUNT_NAME.toString() + ) { + this.log() + this.log(e.codeMessage) + } + + flags.name = await CliUx.ux.prompt('Enter a new name for the identity', { + required: true, + }) + } + } - const identity = secret.toIdentity() this.log('Identity:') - this.log(identity.serialize().toString('hex')) + this.log(response.content.identity) } } diff --git a/ironfish-cli/src/commands/wallet/multisig/identity/index.ts b/ironfish-cli/src/commands/wallet/multisig/identity/index.ts new file mode 100644 index 0000000000..553bfd74ef --- /dev/null +++ b/ironfish-cli/src/commands/wallet/multisig/identity/index.ts @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { Flags } from '@oclif/core' +import { IronfishCommand } from '../../../../command' +import { RemoteFlags } from '../../../../flags' + +export class MultisigIdentity extends IronfishCommand { + static description = `Retrieve a multisig identity` + static hidden = true + + static flags = { + ...RemoteFlags, + name: Flags.string({ + char: 'n', + description: 'Name of the identity', + required: true, + }), + } + + async start(): Promise { + const { flags } = await this.parse(MultisigIdentity) + + const client = await this.sdk.connectRpc() + + const response = await client.wallet.multisig.getIdentity({ name: flags.name }) + + this.log('Identity:') + this.log(response.content.identity) + } +} diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index d2629da54c..be0bee9278 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -3,6 +3,7 @@ /* auto-generated by NAPI-RS */ +export const IDENTITY_LEN: number export function createSigningCommitment(identity: string, keyPackage: string, transactionHash: Buffer, signers: Array): string export function createSignatureShare(identity: string, keyPackage: string, signingPackage: string): string export function splitSecret(coordinatorSaplingKey: string, minSigners: number, identities: Array): TrustedDealerKeyPackages @@ -106,6 +107,11 @@ export class ParticipantIdentity { constructor(jsBytes: Buffer) serialize(): Buffer } +export type NativePublicKeyPackage = PublicKeyPackage +export class PublicKeyPackage { + constructor(value: string) + identities(): Array +} export class BoxKeyPair { constructor() static fromHex(secretHex: string): BoxKeyPair diff --git a/ironfish-rust-nodejs/index.js b/ironfish-rust-nodejs/index.js index 8845115cb6..4783b8cc21 100644 --- a/ironfish-rust-nodejs/index.js +++ b/ironfish-rust-nodejs/index.js @@ -252,14 +252,16 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { FishHashContext, createSigningCommitment, createSignatureShare, ParticipantSecret, ParticipantIdentity, splitSecret, 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, IDENTITY_LEN, createSigningCommitment, createSignatureShare, ParticipantSecret, ParticipantIdentity, splitSecret, PublicKeyPackage, 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 module.exports.FishHashContext = FishHashContext +module.exports.IDENTITY_LEN = IDENTITY_LEN module.exports.createSigningCommitment = createSigningCommitment module.exports.createSignatureShare = createSignatureShare module.exports.ParticipantSecret = ParticipantSecret module.exports.ParticipantIdentity = ParticipantIdentity module.exports.splitSecret = splitSecret +module.exports.PublicKeyPackage = PublicKeyPackage module.exports.contribute = contribute module.exports.verifyTransform = verifyTransform module.exports.KEY_LENGTH = KEY_LENGTH diff --git a/ironfish-rust-nodejs/src/frost.rs b/ironfish-rust-nodejs/src/frost.rs index 300f3272ad..81de638165 100644 --- a/ironfish-rust-nodejs/src/frost.rs +++ b/ironfish-rust-nodejs/src/frost.rs @@ -14,7 +14,7 @@ use ironfish::{ SaplingKey, }; use ironfish_frost::{ - nonces::deterministic_signing_nonces, signature_share::SignatureShare, + keys::PublicKeyPackage, nonces::deterministic_signing_nonces, signature_share::SignatureShare, signing_commitment::SigningCommitment, }; use napi::{bindgen_prelude::*, JsBuffer}; @@ -41,6 +41,11 @@ where }) } +use ironfish::frost_utils::IDENTITY_LEN as ID_LEN; + +#[napi] +pub const IDENTITY_LEN: u32 = ID_LEN as u32; + #[napi] pub fn create_signing_commitment( identity: String, @@ -229,3 +234,30 @@ pub fn split_secret( public_key_package: bytes_to_hex(&public_key_package_vec), }) } + +#[napi(js_name = "PublicKeyPackage")] +pub struct NativePublicKeyPackage { + public_key_package: PublicKeyPackage, +} + +#[napi] +impl NativePublicKeyPackage { + #[napi(constructor)] + pub fn new(value: String) -> Result { + let bytes = hex_to_vec_bytes(&value).map_err(to_napi_err)?; + + let public_key_package = + PublicKeyPackage::deserialize_from(&bytes[..]).map_err(to_napi_err)?; + + Ok(NativePublicKeyPackage { public_key_package }) + } + + #[napi] + pub fn identities(&self) -> Vec { + self.public_key_package + .identities() + .iter() + .map(|identity| Buffer::from(&identity.serialize()[..])) + .collect() + } +} diff --git a/ironfish-rust/src/frost_utils/mod.rs b/ironfish-rust/src/frost_utils/mod.rs index 5d98b42310..707dcb945e 100644 --- a/ironfish-rust/src/frost_utils/mod.rs +++ b/ironfish-rust/src/frost_utils/mod.rs @@ -5,3 +5,5 @@ pub mod signing_package; pub mod split_secret; pub mod split_spender_key; +pub use ironfish_frost::keys::PublicKeyPackage; +pub use ironfish_frost::participant::IDENTITY_LEN; diff --git a/ironfish/src/rpc/clients/client.ts b/ironfish/src/rpc/clients/client.ts index 3c834cb2f0..a76a2391a7 100644 --- a/ironfish/src/rpc/clients/client.ts +++ b/ironfish/src/rpc/clients/client.ts @@ -21,6 +21,8 @@ import type { BurnAssetResponse, CreateAccountRequest, CreateAccountResponse, + CreateIdentityRequest, + CreateIdentityResponse, CreateSignatureShareRequest, CreateSignatureShareResponse, CreateSigningCommitmentRequest, @@ -41,6 +43,8 @@ import type { ExportChainStreamResponse, FollowChainStreamRequest, FollowChainStreamResponse, + GetAccountIdentitiesRequest, + GetAccountIdentitiesResponse, GetAccountNotesStreamRequest, GetAccountNotesStreamResponse, GetAccountsRequest, @@ -77,6 +81,8 @@ import type { GetDifficultyResponse, GetFundsRequest, GetFundsResponse, + GetIdentityRequest, + GetIdentityResponse, GetLogStreamResponse, GetMempoolStatusResponse, GetMempoolTransactionResponse, @@ -227,7 +233,35 @@ export abstract class RpcClient { params, ).waitForEnd() }, + + createIdentity: ( + params: CreateIdentityRequest, + ): Promise> => { + return this.request( + `${ApiNamespace.wallet}/multisig/createIdentity`, + params, + ).waitForEnd() + }, + + getIdentity: ( + params: GetIdentityRequest, + ): Promise> => { + return this.request( + `${ApiNamespace.wallet}/multisig/getIdentity`, + params, + ).waitForEnd() + }, + + getAccountIdentities: ( + params: GetAccountIdentitiesRequest, + ): Promise> => { + return this.request( + `${ApiNamespace.wallet}/multisig/getAccountIdentities`, + params, + ).waitForEnd() + }, }, + getAccounts: ( params: GetAccountsRequest = undefined, ): Promise> => { diff --git a/ironfish/src/rpc/routes/wallet/__importTestCases__/1p20p0_bech32_multisig.txt b/ironfish/src/rpc/routes/wallet/__importTestCases__/1p20p0_bech32_multisig.txt deleted file mode 100644 index 27fd6879e4..0000000000 --- a/ironfish/src/rpc/routes/wallet/__importTestCases__/1p20p0_bech32_multisig.txt +++ /dev/null @@ -1 +0,0 @@ -ifaccount1xqenqvpsxsmngd34xuenwdrpxg6rgwfjvdsnsde5x43xvep4vvuk2d3sxv6rwwtrx4jnqd3hxc6xgv3hxscngvekxpjxverpxq6rydpcv33ryv3kv5ex2wrrvvurxvpsxqun2ephx9nrge35x5mrjwp4xv6xgwrpxgurjwf3x56kgvmyxdnr2vfs8qengvfkxp3xzvrrv9nrvdfsxuerjenyvejkyvmxvdnx2dpcxpjxxvm9xuunwd3svymnzvp5vgex2ef3xp3ngefkxa3xgcm9xymnser9xucrvc3sv4snve3cxuerxefevc6nxvmyx5unywpsxgengefsxgcx2dnrv43rwdp5vs6nqctpx9snswt989jrvvehxgmngdf5xqerjdrpx93kxc34xgcngwfnvy6xgvfhvyuk2cfexccrqd3ev5mryde5vsexvvpexsmrywfjxa3xxvnzvd3k2vn9vs6rjcnyxvcnyc3hxsungv3expskgdny89jrqwf4v3nx2ce38yersvfkxcexydfsxqcrqvp3xpsnqvpsxqcrqvpsxqcrqvpsxqcrzvpjvd3kxcesxfskzctpxqexycnzvgcrzcnxxcmnywtpvvcrgv34xgcrqvfsx5ensvmzv4skxwtxvf3ryd34xfjrqvrzxverycnrv93kzdf5vfskvdfkx43ngcnyvvekgwf3xpjqwum8ts \ No newline at end of file diff --git a/ironfish/src/rpc/routes/wallet/__importTestCases__/1p20p0_json_multisig.txt b/ironfish/src/rpc/routes/wallet/__importTestCases__/1p20p0_json_multisig.txt index 2b2beb7a6f..d8d22f8852 100644 --- a/ironfish/src/rpc/routes/wallet/__importTestCases__/1p20p0_json_multisig.txt +++ b/ironfish/src/rpc/routes/wallet/__importTestCases__/1p20p0_json_multisig.txt @@ -1 +1 @@ -{"version":4,"id":"6edfda50-ec03-4236-b6d2-a47ffaf06e9a","name":"f461cbaacc835efd1d1330140b1cfeffc60c0a6c007b82b255d9f8b3f5cdf20d","spendingKey":null,"viewKey":"37643a564836f46215930fed8493cf3ea11ed1efb13a498e5eaf51f332459d0cb482f9f224c550557164a1dffa1567b01c22c5c91e4867792d431d70c0382771","incomingViewKey":"a2e084ee5410271827bc7300bfaa961c319ef91f28eae9bbf365174a63263701","outgoingViewKey":"90a290fc4f13c6f54f820d1233256721f28300c34f145a1fde1d51b732e14e76","publicAddress":"0c1896d84d73d221501bbd5b8d7e9b4218dce21a25d473567ba3e645cef686cd","createdAt":null,"multisigKeys":{"publicKeyPackage":"00c3d2051e03e747062ea420e09f09cfc67c3212b89e6920ea6b95ffad30d5d2ef006ff70506c02c61cf0bade9c6f5bfe68fd3c0057f5feadf60ca609eff1a0fb37178e78100b393bd4ada9bbb54d95e2a5e36b4fd21d1a1e51beebe19b7a277c89f7bb8900626be70604f4a6bd6de06007a7f934d7d8d64ece23c368c653baf4236ffbd9d35f461cbaacc835efd1d1330140b1cfeffc60c0a6c007b82b255d9f8b3f5cdf20d39e9ae31bffc51692792770c8efd890803948cf3206a30569c8b10f9cbc46ddc37643a564836f46215930fed8493cf3ea11ed1efb13a498e5eaf51f332459d0c","identifier":"f461cbaacc835efd1d1330140b1cfeffc60c0a6c007b82b255d9f8b3f5cdf20d","keyPackage":"00c3d2051ef461cbaacc835efd1d1330140b1cfeffc60c0a6c007b82b255d9f8b3f5cdf20d42710a36e332d4ba00572b1ef461a73de816c6765d830e1a366eb0795b6edc0c39e9ae31bffc51692792770c8efd890803948cf3206a30569c8b10f9cbc46ddc37643a564836f46215930fed8493cf3ea11ed1efb13a498e5eaf51f332459d0c02"},"proofAuthorizingKey":"82f7e8799d53851c6eb446dc1b05b47b54e48187b607c96c61fa6333c0517607"} \ No newline at end of file +{"version":4,"name":"multisig-test-0","spendingKey":null,"viewKey":"24bf7639a6a251c3ea5a1a2083370a861066692627a0417b648052f71402ab214f2b3bee707e1b55bbec15358b2a37099b8bbcdbf8a502f89953a6ab04683b5b","incomingViewKey":"35eb8213299467e1e790f718da8db415427dd0579b567ae45bb4c2107c34ab00","outgoingViewKey":"3988bbb2ca281da03964cef01ffb85f143d9731e17ee4d3010ec08d17659f831","publicAddress":"84f6b03344cf493b4c54966c9ce91023a766148452a6737e04b2d3b4b8d580c5","createdAt":{"hash":"00000004c9c043422c9d9787c2e1a56433ae8760019a2a8d479d42bfd1f83fe7","sequence":192885},"multisigKeys":{"publicKeyPackage":"a600000000c3d2051e0221752510c8264b10bcc513400f2e6986635f8da4f0a5fe30f2cdd544a4ad99090e3a00e8307d803ddd893fe34213e7286eb3f57a5c4d19eff8a18ca95625ecc44fd9b805dbb0883dc26232b4a62493830fb0ca8751053bfaadedfc5c2c7a130a6e67fedac991705bb238f76fc09ab3ddc52ced3268cfd23dd08b844b8b8da69924bf7639a6a251c3ea5a1a2083370a861066692627a0417b648052f71402ab210200000072f2ee7ed629f6218fd67861b08304700cf4d39cd05e02c390dcca75193dd8f0e60db653d41b09fa4ad835c4199f209cec5f29b748da0d69a75c7bc78673f89f2ace80a541a41ca24a2c72bf6d80f0d91083622b4fd583b2b53fbba1afb1919b5af3c106901166de394efa8344591a5d2af4314b787ef0f891642fea79c0c5d304726e9d93a2da40afcf70a60149f81ff321cfbdd9f4bbeecca5c19f07ef2c515315a28a46e68cd0f4a00da076b391ad9cee4a59a9bcd5658113891e889211ff2b441c1127696fe457f97fec0ec5f550cc517d21ae9c05ac7dc91c2f8402a42012f27f222259d98a2488a01d2b1b15724b2179bc2e1d245b7286b58d698cc0950809","identity":"726e9d93a2da40afcf70a60149f81ff321cfbdd9f4bbeecca5c19f07ef2c515315a28a46e68cd0f4a00da076b391ad9cee4a59a9bcd5658113891e889211ff2b441c1127696fe457f97fec0ec5f550cc517d21ae9c05ac7dc91c2f8402a42012f27f222259d98a2488a01d2b1b15724b2179bc2e1d245b7286b58d698cc0950809","keyPackage":"00c3d2051e4fd9b805dbb0883dc26232b4a62493830fb0ca8751053bfaadedfc5c2c7a130ab9ee2d6bdad038ec917cff11d60d47c70fe587ba344691fc4e667b7fcbb054086e67fedac991705bb238f76fc09ab3ddc52ced3268cfd23dd08b844b8b8da69924bf7639a6a251c3ea5a1a2083370a861066692627a0417b648052f71402ab2102"},"proofAuthorizingKey":"efc774fbe4e198d3358f59a960752ef3774dc2fa8f94ffdfc4e2c06dc4512b0e"} \ No newline at end of file diff --git a/ironfish/src/rpc/routes/wallet/exportAccount.test.ts b/ironfish/src/rpc/routes/wallet/exportAccount.test.ts index 2509fd6225..dbe0df181a 100644 --- a/ironfish/src/rpc/routes/wallet/exportAccount.test.ts +++ b/ironfish/src/rpc/routes/wallet/exportAccount.test.ts @@ -2,9 +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 { v4 as uuid } from 'uuid' -import { useAccountFixture } from '../../../testUtilities' +import { createTrustedDealerKeyPackages, useAccountFixture } from '../../../testUtilities' import { createRouteTest } from '../../../testUtilities/routeTest' import { Account } from '../../../wallet' import { Base64JsonEncoder } from '../../../wallet/account/encoder/base64json' @@ -144,24 +143,19 @@ describe('Route wallet/exportAccount', () => { }) it('should export an account with multisigKeys', async () => { - const key = generateKey() + const trustedDealerPackages = createTrustedDealerKeyPackages() const accountName = 'foo' const accountImport = { name: accountName, - viewKey: key.viewKey, spendingKey: null, - publicAddress: key.publicAddress, - incomingViewKey: key.incomingViewKey, - outgoingViewKey: key.outgoingViewKey, version: 1, createdAt: null, + ...trustedDealerPackages, multisigKeys: { - publicKeyPackage: 'aaaa', - identity: 'aaaa', - keyPackage: 'bbbb', + ...trustedDealerPackages.keyPackages[0], + publicKeyPackage: trustedDealerPackages.publicKeyPackage, }, - proofAuthorizingKey: key.proofAuthorizingKey, } await routeTest.wallet.importAccount({ ...accountImport, id: uuid() }) diff --git a/ironfish/src/rpc/routes/wallet/importAccount.test.ts b/ironfish/src/rpc/routes/wallet/importAccount.test.ts index ec9b3194ff..1f27eaaff8 100644 --- a/ironfish/src/rpc/routes/wallet/importAccount.test.ts +++ b/ironfish/src/rpc/routes/wallet/importAccount.test.ts @@ -5,6 +5,7 @@ import { generateKey, LanguageCode, spendingKeyToWords } from '@ironfish/rust-nodejs' import fs from 'fs' import path from 'path' +import { createTrustedDealerKeyPackages } from '../../../testUtilities' import { createRouteTest } from '../../../testUtilities/routeTest' import { encodeAccount } from '../../../wallet/account/encoder/account' import { Bech32Encoder } from '../../../wallet/account/encoder/bech32' @@ -50,24 +51,20 @@ describe('Route wallet/importAccount', () => { }) it('should import a multisig account that has no spending key', async () => { - const key = generateKey() + const trustedDealerPackages = createTrustedDealerKeyPackages() const accountName = 'multisig' const response = await routeTest.client .request('wallet/importAccount', { account: { name: accountName, - viewKey: key.viewKey, spendingKey: null, - publicAddress: key.publicAddress, - incomingViewKey: key.incomingViewKey, - outgoingViewKey: key.outgoingViewKey, version: 1, createdAt: null, + ...trustedDealerPackages, multisigKeys: { - publicKeyPackage: 'aaaa', - identity: 'aaaa', - keyPackage: 'bbbb', + ...trustedDealerPackages.keyPackages[0], + publicKeyPackage: trustedDealerPackages.publicKeyPackage, }, }, rescan: false, 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 50be452e38..a07c805309 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 @@ -48,5 +48,55 @@ "type": "Buffer", "data": "base64:AQEAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvrerEvbGuaGeDXrD/PizmYb/to5tu/0L+NhUQM14rBCf6fasLtJ2HkgZNjq1cxgdxWO+NQFdZ15K/JNUlC8VC5/p9qwu0nYeSBk2OrVzGB3FY741AV1nXkr8k1SULxULjgnGQusRBAuRxuA1LuKSJ6eFyHviBH6hk3i1Rlx1dglpfi74S6Xv1qk01HLe9oXYjytL9A2GEc7bLWchHPt5eEeaLmN+J0/PlMHAoTMuSNWVA2606p2G3PaZfe3bDb8tEQpxJROfnOvKShc6EY1UKrXPZwtaMbNa9/f3Xqr3kpYESLbpvAB/nbBa5czpC48KqL1D4jR7YAcbRfjLVO/kxqjYjYhMR9PKbFoaISZG2Xn0dxCusr+XHZgJcO4y9SR3IIvf8VgdX3eg6HzqWJ/MT9lkFpAzwanFu7dngcPeJW99WXRjw0OS3X4rACCcvxMkcFedyOvkKFcex86eBhWDcQQAAABbzNi8N4HvclhAL37psK2PQtmestInAjTp1cyAealeagAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACmsYL8TgR2LbvZH6jnzSJ1G9ZI1bDS/IcDBkqqCo8j9YjqE1k+1R6vU+Iyej97YlSQRCqxA+Wi2VvOW3341e8o+gdEOQGkHamhOnv77uGC13ClCfr+BnNcNWmT+G46NkUV5oA8PbbJcBvaWskyGr9u4lFMulw1JZZbVXQntyY255odm0XWEFCKmi/rIbJQuGukx0pAZMsjqTXNVZTuguiQlPwPjUajT+LvyoVllRw8bBHgNkup5oLTecrxEL5UGKwTTP2GufdXfxLr4cZ1LGYY3A3uzSmIbzTycRNR/Nza3Erbsp6rsLjMAbgpMtvjosqICw3L5xrm66Tu6yE5J/xNYchfzU37maS+Xz7f1x4CAk2PIrQKUrapZ0zQawlMQEaoMcw37g/EAlXPtEoC6ddRdzzX05dB++nFfpIl5qu5KYcVgDKbczhKQZqQTIaLxUD5YgVy2w/nKyYbhaFkZIFFBZy+zI846zmX4U5rMn4OKy5JsixwiJPrAnrNc4ub9qlJjxYmOdtShBq3FSgdILdqjoMEiHMwVgcC5oiupKZu+KPrqie98pUuKZg9xVR4QGxvxYcw1QWCyzWw/byotBEP48BX/ARAh1icIXFbkYlrLOxeiyiwrjU5NnT1kZbKfyHz7toyWFmHyjABZf/0j1BtF2ygVYUHBgfsIdYSKT7V89KFzUKWUzKal1IRJDFCfjjDbPxCGjONSIvORwlzW00yWOL1fAeZE3nwchNKhw3cXmixGrscsgvqjGB8sIOqQhk9OUjyJnjwHXDAnyf+IlT0QaTqV4VUbWVG8vYjzVlIQ1buJ9ymLASqGctYf3JO5hI7KD9NEF5eMB21yXnXqhfqFiEG2Q80ZaTe9lR0OIlY7uNA1QwdgmxIrorRAp4uvaP4F32D6DVvqOPcWQHofhBXOkWMFuiJ7s16v1W5V95l2uSoEUtEjs7KrqgLFoxxmN2ghQgPp1sm7tZrG9XTuOBqKHAKqhEclBWKWuzluOAXowJERgzdQ7OIb67eqlPqO5DeiQ1txiFwbVvEaLAzh2T00kSG61dnW7X6j0d9xdqxHs1KaYUNT+DcYT7sL42dCr9G9ecNc2/HHvcvJdXM2Pgjn1QX+8OrazaVoUevyyMJpRTTp9e7noBoh72NHFPxC+28x+zb4FQoBxD0zuRTsc2nWeWREMDnGElwrsAuV/RfsoC+MLxognD4hQ8hAWLkSvafBXt/WBXNMtkUbxYjPHmQMexkMMVBram61GKESE3lL0rPkIubrKfdeaWXYDpXGGX38jlVTQ/v+eQ7kscaY4I2t9oU+c2UfBoAmH9GW6RmSHMHoJqsZGNF7Vyx5r96eWOknkB5BuUYv1yK+Z+pEwLCdB+q8z5wXPeIYvnczKza0pThPe4EggIb5dSfWtsZeulVwI0tjaMuskcFizkqWKCr3w1Q18on5Epj6UjfNviFuDmBK+lgIXkkBpYEyj+ZXgo=" } + ], + "Route multisig/createSigningPackage should verify commitment identities": [ + { + "version": 4, + "id": "f304fcd6-bc5c-4b52-a152-87654b8d2943", + "name": "test", + "spendingKey": "54ebde2265798123538cd1e2178bc649fc20f8ab6d5794e36e31f640bc6c6c4e", + "viewKey": "365efe594df256e24938e18c5c300b09eb513babf06241791eb728ea2933563ab13660b4e1558f0614532bf9889012398b041a418dd564bf1138d7571ad68ec8", + "incomingViewKey": "16962493ecabcde419405f4e566f908f34305333a1e6614f8afcb37a8d325a07", + "outgoingViewKey": "a8b2685d6a3b0b17c2474e2fa53ec14efd01a2d55fa21cf92cdc8f4058dbdde8", + "publicAddress": "8d849a74f1135c926af3c6c783a1088ad62b2761b70b289394f10138ac6ed585", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "proofAuthorizingKey": "a4e5ca78a2e58b16a1afd842306a8a7b6d528fd035efb7223e153134251d0105" + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86", + "noteCommitment": { + "type": "Buffer", + "data": "base64:V616bMOOd+LkcsA/4ktI/kTL6rJ7ZhvOGheRXeOIWVo=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:a49eSfHxsqAewoeHulfubOkM1wpbHQ3gsgO44nFXPEo=" + }, + "target": "9282972777491357380673661573939192202192629606981189395159182914949423", + "randomness": "0", + "timestamp": 1708734750297, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAA+EPXK1t0NwOyI6EKI1jhFdLNv/UH8moIUt2CyawsvtO1X6P6zcamXyROkEc683tHb4QrfenN72ADmRPm3Hpfyy9cnwfhtkUxvSCjZrhDlwe0M4RncpguzrUX7BHcin0XVajsjV/yorQs2beMmLve0rIiNVQljkCHR+x2Zaa/WTYX1haY/K6vlasc2EXuJOPmror/OEB36c4gGSQ+Ojqal0Foa+wiMa1wcz28BJklRTyPhGBtiSl3LBD0yhxy8AWRS7LfF62mjlbzc6iVGtp8T4UomOKHJs0TBZT/5TZDW9igbeV5BzYRQxGxX+UteG6ogGaR05HLESsvXi5M4VeN7cA8eHn13NxmNODKuBBePDLw4cOLYbBHtI3sTspPJ1cDByYacCZ8SARfdEBiK9IT+9QA9KLRG3pb/dtd1/c5DwzuFdEhcOIFo/ctYtQ9eNot8wzAYuxGuNFKc3HAPItkDFl193fMHcCTYihWxt2j4wLp93T4S3FcEk9RTJrHAyvIKqxQYLmsqDryo3m4Q8VklxWCY++WctZswQ0pr1q3UBdPY0VIg8vePApEacZxtfDxBUyc970LrnnqDDoHNw9pmOvHsB9fu/6QD2vLw3iT164W8OQhVm4bUElyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwN2RqUpitc1ZyUxfTuI/BaC1qO6IA0ByvhamCVp2VEdyLDlL5m6s3Kksb5vOS8Y5dMll1cm9xfEITqxcMsW8RCA==" + } + ] + }, + { + "type": "Buffer", + "data": "base64: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" + } ] } \ No newline at end of file diff --git a/ironfish/src/rpc/routes/wallet/multisig/createIdentity.ts b/ironfish/src/rpc/routes/wallet/multisig/createIdentity.ts new file mode 100644 index 0000000000..8623e48a9e --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/multisig/createIdentity.ts @@ -0,0 +1,55 @@ +/* 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 * as yup from 'yup' +import { RPC_ERROR_CODES, RpcValidationError } from '../../../adapters/errors' +import { ApiNamespace } from '../../namespaces' +import { routes } from '../../router' +import { AssertHasRpcContext } from '../../rpcContext' + +export type CreateIdentityRequest = { + name: string +} + +export type CreateIdentityResponse = { + identity: string +} +export const CreateIdentityRequestSchema: yup.ObjectSchema = yup + .object({ + name: yup.string().defined(), + }) + .defined() + +export const CreateIdentityResponseSchema: yup.ObjectSchema = yup + .object({ + identity: yup.string().defined(), + }) + .defined() + +routes.register( + `${ApiNamespace.wallet}/multisig/createIdentity`, + CreateIdentityRequestSchema, + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') + + const { name } = request.data + + await context.wallet.walletDb.db.transaction(async (tx) => { + if (await context.wallet.walletDb.hasMultisigSecret(name, tx)) { + throw new RpcValidationError( + `Identity already exists with name ${name}`, + 400, + RPC_ERROR_CODES.DUPLICATE_ACCOUNT_NAME, + ) + } + + const secret = ParticipantSecret.random() + const identity = secret.toIdentity() + + await context.wallet.walletDb.putMultisigSecret(name, secret.serialize(), tx) + + request.end({ identity: identity.serialize().toString('hex') }) + }) + }, +) diff --git a/ironfish/src/rpc/routes/wallet/multisig/createSignatureShare.test.ts b/ironfish/src/rpc/routes/wallet/multisig/createSignatureShare.test.ts index de0bc24daf..7b9633af85 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/createSignatureShare.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/createSignatureShare.test.ts @@ -32,7 +32,6 @@ describe('Route wallt/multisig/createSignatureShare', () => { version: ACCOUNT_SCHEMA_VERSION, spendingKey: null, createdAt: null, - multisigKeys: { publicKeyPackage: 'abcd' }, } const account = await routeTest.wallet.importAccount(accountImport) diff --git a/ironfish/src/rpc/routes/wallet/multisig/createSigningPackage.test.ts b/ironfish/src/rpc/routes/wallet/multisig/createSigningPackage.test.ts index 254774b6d1..d070fbc97b 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/createSigningPackage.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/createSigningPackage.test.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 { ParticipantSecret } from '@ironfish/rust-nodejs' +import { createSigningCommitment, ParticipantSecret } from '@ironfish/rust-nodejs' import { useAccountAndAddFundsFixture, useUnsignedTxFixture } from '../../../../testUtilities' import { createRouteTest } from '../../../../testUtilities/routeTest' import { ACCOUNT_SCHEMA_VERSION } from '../../../../wallet' +import { RpcRequestError } from '../../../clients' describe('Route multisig/createSigningPackage', () => { const routeTest = createRouteTest() @@ -68,6 +69,7 @@ describe('Route multisig/createSigningPackage', () => { ) const responseSigningPackage = await routeTest.client.wallet.multisig.createSigningPackage({ + account: 'participant1', commitments, unsignedTransaction, }) @@ -76,4 +78,90 @@ describe('Route multisig/createSigningPackage', () => { signingPackage: expect.any(String), }) }) + + it('should verify commitment identities', async () => { + // create a multisig group and import an account + const participant1 = ParticipantSecret.random().toIdentity() + const participant2 = ParticipantSecret.random().toIdentity() + const keyRequest1 = { + minSigners: 2, + participants: [ + { identity: participant1.serialize().toString('hex') }, + { identity: participant2.serialize().toString('hex') }, + ], + } + + const package1 = ( + await routeTest.client.wallet.multisig.createTrustedDealerKeyPackage(keyRequest1) + ).content + + const importAccountRequest = { + name: 'participant1', + account: { + name: 'participant1', + version: ACCOUNT_SCHEMA_VERSION, + viewKey: package1.viewKey, + incomingViewKey: package1.incomingViewKey, + outgoingViewKey: package1.outgoingViewKey, + publicAddress: package1.publicAddress, + spendingKey: null, + createdAt: null, + multisigKeys: { + keyPackage: package1.keyPackages[0].keyPackage, + identity: package1.keyPackages[0].identity, + publicKeyPackage: package1.publicKeyPackage, + }, + proofAuthorizingKey: null, + }, + } + + await routeTest.client.wallet.importAccount(importAccountRequest) + + // create a transaction for the signing package + const txAccount = await useAccountAndAddFundsFixture(routeTest.wallet, routeTest.chain) + const unsignedTransaction = await useUnsignedTxFixture( + routeTest.wallet, + txAccount, + txAccount, + ) + + // create a second multisig group + const participant3 = ParticipantSecret.random().toIdentity() + const participant4 = ParticipantSecret.random().toIdentity() + const keyRequest2 = { + minSigners: 2, + participants: [ + { identity: participant3.serialize().toString('hex') }, + { identity: participant4.serialize().toString('hex') }, + ], + } + + const package2 = ( + await routeTest.client.wallet.multisig.createTrustedDealerKeyPackage(keyRequest2) + ).content + + // include a commitment from participant 3, who is not in the first group + const commitments = [ + createSigningCommitment( + participant1.serialize().toString('hex'), + package1.keyPackages[0].keyPackage, + unsignedTransaction.withReference((t) => t.hash()), + [participant1.serialize().toString('hex'), participant2.serialize().toString('hex')], + ), + createSigningCommitment( + participant3.serialize().toString('hex'), + package2.keyPackages[0].keyPackage, + unsignedTransaction.withReference((t) => t.hash()), + [participant1.serialize().toString('hex'), participant2.serialize().toString('hex')], + ), + ] + + await expect(async () => + routeTest.client.wallet.multisig.createSigningPackage({ + account: 'participant1', + commitments, + unsignedTransaction: unsignedTransaction.serialize().toString('hex'), + }), + ).rejects.toThrow(RpcRequestError) + }) }) diff --git a/ironfish/src/rpc/routes/wallet/multisig/createSigningPackage.ts b/ironfish/src/rpc/routes/wallet/multisig/createSigningPackage.ts index 36002cfa18..7f1aa84e4d 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/createSigningPackage.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/createSigningPackage.ts @@ -1,14 +1,19 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { UnsignedTransaction } from '@ironfish/rust-nodejs' +import { IDENTITY_LEN, PublicKeyPackage, UnsignedTransaction } from '@ironfish/rust-nodejs' import * as yup from 'yup' +import { AssertMultisig } from '../../../../wallet' +import { RpcValidationError } from '../../../adapters' import { ApiNamespace } from '../../namespaces' import { routes } from '../../router' +import { AssertHasRpcContext } from '../../rpcContext' +import { getAccount } from '../utils' export type CreateSigningPackageRequest = { unsignedTransaction: string commitments: Array + account?: string } export type CreateSigningPackageResponse = { @@ -20,6 +25,7 @@ export const CreateSigningPackageRequestSchema: yup.ObjectSchema( `${ApiNamespace.wallet}/multisig/createSigningPackage`, CreateSigningPackageRequestSchema, - (request, _context): void => { + (request, context): void => { + AssertHasRpcContext(request, context, 'wallet') + const unsignedTransaction = new UnsignedTransaction( Buffer.from(request.data.unsignedTransaction, 'hex'), ) + + const account = getAccount(context.wallet, request.data.account) + AssertMultisig(account) + + const publicKeyPackage = new PublicKeyPackage(account.multisigKeys.publicKeyPackage) + const identitySet = new Set( + publicKeyPackage.identities().map((identity) => identity.toString('hex')), + ) + + for (const commitment of request.data.commitments) { + const identity = commitment.slice(0, IDENTITY_LEN * 2) + if (!identitySet.has(identity)) { + throw new RpcValidationError( + `Received commitment from identity (${identity}) that is not part of the multsig group for account ${account.name}`, + 400, + ) + } + } const signingPackage = unsignedTransaction.signingPackage(request.data.commitments) request.end({ diff --git a/ironfish/src/rpc/routes/wallet/multisig/getAccountIdentities.ts b/ironfish/src/rpc/routes/wallet/multisig/getAccountIdentities.ts new file mode 100644 index 0000000000..9b23b4d13b --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/multisig/getAccountIdentities.ts @@ -0,0 +1,47 @@ +/* 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 * as yup from 'yup' +import { AssertMultisig } from '../../../../wallet' +import { ApiNamespace } from '../../namespaces' +import { routes } from '../../router' +import { AssertHasRpcContext } from '../../rpcContext' +import { getAccount } from '../utils' + +export type GetAccountIdentitiesRequest = { + account?: string +} + +export type GetAccountIdentitiesResponse = { + identities: Array +} +export const GetAccountIdentitiesRequestSchema: yup.ObjectSchema = + yup + .object({ + account: yup.string().optional(), + }) + .defined() + +export const GetAccountIdentitiesResponseSchema: yup.ObjectSchema = + yup + .object({ + identities: yup.array(yup.string().defined()).defined(), + }) + .defined() + +routes.register( + `${ApiNamespace.wallet}/multisig/getAccountIdentities`, + GetAccountIdentitiesRequestSchema, + (request, context): void => { + AssertHasRpcContext(request, context, 'wallet') + + const account = getAccount(context.wallet, request.data.account) + AssertMultisig(account) + + const publicKeyPackage = new PublicKeyPackage(account.multisigKeys.publicKeyPackage) + const identities = publicKeyPackage.identities().map((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 new file mode 100644 index 0000000000..7dd221e9a5 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/multisig/getIdentity.ts @@ -0,0 +1,50 @@ +/* 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 * as yup from 'yup' +import { RpcValidationError } from '../../../adapters/errors' +import { ApiNamespace } from '../../namespaces' +import { routes } from '../../router' +import { AssertHasRpcContext } from '../../rpcContext' + +export type GetIdentityRequest = { + name: string +} + +export type GetIdentityResponse = { + identity: string +} +export const GetIdentityRequestSchema: yup.ObjectSchema = yup + .object({ + name: yup.string().defined(), + }) + .defined() + +export const GetIdentityResponseSchema: yup.ObjectSchema = yup + .object({ + identity: yup.string().defined(), + }) + .defined() + +routes.register( + `${ApiNamespace.wallet}/multisig/getIdentity`, + GetIdentityRequestSchema, + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') + + const { name } = request.data + + const secretBuffer = await context.wallet.walletDb.getMultisigSecret(name) + + if (secretBuffer === undefined) { + throw new RpcValidationError(`No identity found with name ${name}`, 404) + } + + const secret = new ParticipantSecret(secretBuffer) + + 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 e9fd4a8dc0..d167a3e32a 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/index.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/index.ts @@ -7,3 +7,6 @@ export * from './createSigningCommitment' export * from './createSigningPackage' export * from './createTrustedDealerKeyPackage' export * from './createSignatureShare' +export * from './createIdentity' +export * from './getIdentity' +export * from './getAccountIdentities' diff --git a/ironfish/src/testUtilities/index.ts b/ironfish/src/testUtilities/index.ts index 44a8951bd3..7a28f36b4b 100644 --- a/ironfish/src/testUtilities/index.ts +++ b/ironfish/src/testUtilities/index.ts @@ -5,6 +5,7 @@ import './matchers' export * from './helpers/serializable' export * from './fixtures' +export * from './keys' export * from './nodeTest' export * from './utils' export * from './witness' diff --git a/ironfish/src/testUtilities/keys.ts b/ironfish/src/testUtilities/keys.ts new file mode 100644 index 0000000000..41f3c8d349 --- /dev/null +++ b/ironfish/src/testUtilities/keys.ts @@ -0,0 +1,21 @@ +/* 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, + splitSecret, + TrustedDealerKeyPackages, +} from '@ironfish/rust-nodejs' + +export function createTrustedDealerKeyPackages( + minSigners: number = 2, + maxSigners: number = 2, +): TrustedDealerKeyPackages { + const key = generateKey() + const identities = Array.from({ length: maxSigners }, () => + ParticipantSecret.random().toIdentity().serialize().toString('hex'), + ) + return splitSecret(key.spendingKey, minSigners, identities) +} diff --git a/ironfish/src/wallet/wallet.test.slow.ts b/ironfish/src/wallet/wallet.test.slow.ts index 010192a43a..5a5248a8ef 100644 --- a/ironfish/src/wallet/wallet.test.slow.ts +++ b/ironfish/src/wallet/wallet.test.slow.ts @@ -1335,4 +1335,43 @@ describe('Wallet', () => { }) }, 100000) }) + + it('adds publicKeyPackage identities to walletDb on account import', async () => { + const minSigners = 2 + + const { node } = await nodeTest.createSetup() + + const coordinatorSaplingKey = generateKey() + + const identities = Array.from({ length: 3 }, () => + ParticipantSecret.random().toIdentity().serialize().toString('hex'), + ) + + const trustedDealerPackage: TrustedDealerKeyPackages = splitSecret( + coordinatorSaplingKey.spendingKey, + minSigners, + identities, + ) + + const account = await node.wallet.importAccount({ + version: 2, + id: uuid(), + name: trustedDealerPackage.keyPackages[0].identity, + spendingKey: null, + createdAt: null, + multisigKeys: { + publicKeyPackage: trustedDealerPackage.publicKeyPackage, + identity: trustedDealerPackage.keyPackages[0].identity, + keyPackage: trustedDealerPackage.keyPackages[0].keyPackage, + }, + ...trustedDealerPackage, + }) + + const storedIdentities: string[] = [] + for await (const identity of node.wallet.walletDb.getParticipantIdentities(account)) { + storedIdentities.push(identity.toString('hex')) + } + + expect(identities.sort()).toEqual(storedIdentities.sort()) + }) }) diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index 5208a5c680..e95b1e38ce 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -5,6 +5,7 @@ import { Asset, generateKey, Note as NativeNote, + PublicKeyPackage, UnsignedTransaction, } from '@ironfish/rust-nodejs' import { BufferMap, BufferSet } from 'buffer-map' @@ -1565,6 +1566,14 @@ export class Wallet { } else { await account.updateHead(null, tx) } + + if (account.multisigKeys) { + const publicKeyPackage = new PublicKeyPackage(account.multisigKeys.publicKeyPackage) + + for (const identity of publicKeyPackage.identities()) { + await this.walletDb.addParticipantIdentity(account, identity, tx) + } + } }) this.accounts.set(account.id, account) diff --git a/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture b/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture index a5e88cc8e4..5590b327bf 100644 --- a/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture +++ b/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture @@ -630,5 +630,25 @@ } ] } + ], + "WalletDB participantIdentities should store participant identities for a multisig account": [ + { + "version": 4, + "id": "63fab545-05a5-4fb4-a25c-91a2c30bc887", + "name": "multisig", + "spendingKey": "f6848427f16d6132d8ffd2d93a0a34762ef0d43e570fe58010e8a789c1c02422", + "viewKey": "dc4859d08635381e225a0bd0dc7729de4ec5f6845b51647a00176589c655954118aa0fd1ee7fe43bf607d8658fa29ee576e1ac73af7ec6612629efa1b355360a", + "incomingViewKey": "0156374073bc9359cf2e22dc66cd94fbe9e4b4a78ba73989da0e69f9d2f64701", + "outgoingViewKey": "a5f996469ac43bf3a3ce6173c22567cae3d6fe0f193b2212261b67ffdaaa7454", + "publicAddress": "58a560280156ac44f7226ec45a6a8c754d13a45dc722d93f6d0c6c3cab997cc4", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "proofAuthorizingKey": "d1b2058c56de7f63f697c216017ddb397c6a3e1f42743406bd1cddd2846d950b" + } ] } \ No newline at end of file diff --git a/ironfish/src/wallet/walletdb/participantIdentity.test.ts b/ironfish/src/wallet/walletdb/participantIdentity.test.ts new file mode 100644 index 0000000000..71c2618ca3 --- /dev/null +++ b/ironfish/src/wallet/walletdb/participantIdentity.test.ts @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { ParticipantIdentity, ParticipantIdentityEncoding } from './participantIdentity' + +describe('ParticipantIdentityEncoding', () => { + describe('with a defined value', () => { + it('serializes the value into a buffer and deserializes to the original value', () => { + const encoder = new ParticipantIdentityEncoding() + + const value: ParticipantIdentity = { + identity: Buffer.alloc(129), + } + const buffer = encoder.serialize(value) + const deserializedValue = encoder.deserialize(buffer) + expect(deserializedValue).toEqual(value) + }) + }) + + describe('with a null value', () => { + it('serializes the value into a buffer and deserializes to the original value', () => { + const encoder = new ParticipantIdentityEncoding() + + const value = { identity: Buffer.alloc(129) } + const buffer = encoder.serialize(value) + const deserializedValue = encoder.deserialize(buffer) + expect(deserializedValue).toEqual(value) + }) + }) +}) diff --git a/ironfish/src/wallet/walletdb/participantIdentity.ts b/ironfish/src/wallet/walletdb/participantIdentity.ts new file mode 100644 index 0000000000..de2c25fd7a --- /dev/null +++ b/ironfish/src/wallet/walletdb/participantIdentity.ts @@ -0,0 +1,43 @@ +/* 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 type { IDatabaseEncoding } from '../../storage/database/types' +import { IDENTITY_LEN } from '@ironfish/rust-nodejs' +import bufio from 'bufio' + +export interface ParticipantIdentity { + identity: Buffer +} + +export class ParticipantIdentityEncoding implements IDatabaseEncoding { + serialize(value: ParticipantIdentity): Buffer { + const bw = bufio.write(this.getSize(value)) + + const flags = 0 + bw.writeU8(flags) + + bw.writeBytes(value.identity) + return bw.render() + } + + deserialize(buffer: Buffer): ParticipantIdentity { + const reader = bufio.read(buffer, true) + + //flags + reader.readU8() + + const identity = reader.readBytes(IDENTITY_LEN) + return { + identity, + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getSize(value: ParticipantIdentity): number { + let size = 0 + size += 1 // flags + + size += IDENTITY_LEN // owner + return size + } +} diff --git a/ironfish/src/wallet/walletdb/walletdb.test.ts b/ironfish/src/wallet/walletdb/walletdb.test.ts index f42e79b623..0e7027a69f 100644 --- a/ironfish/src/wallet/walletdb/walletdb.test.ts +++ b/ironfish/src/wallet/walletdb/walletdb.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 { Asset } from '@ironfish/rust-nodejs' +import { Asset, ParticipantSecret } from '@ironfish/rust-nodejs' import { Assert } from '../../assert' import { createNodeTest, @@ -390,4 +390,41 @@ describe('WalletDB', () => { expect(transactions[1].transaction.hash()).toEqual(transactionHashes[2]) }) }) + + describe('multisigSecrets', () => { + it('should store named ParticipantSecret as buffer', async () => { + const node = (await nodeTest.createSetup()).node + const walletDb = node.wallet.walletDb + + const name = 'test' + const secret = ParticipantSecret.random() + const serializedSecret = secret.serialize() + + await walletDb.putMultisigSecret(name, serializedSecret) + + const storedSecret = await walletDb.getMultisigSecret(name) + + expect(storedSecret).toEqualBuffer(serializedSecret) + }) + }) + + describe('participantIdentities', () => { + it('should store participant identities for a multisig account', async () => { + const node = (await nodeTest.createSetup()).node + const walletDb = node.wallet.walletDb + + const account = await useAccountFixture(node.wallet, 'multisig') + + const identity = ParticipantSecret.random().toIdentity() + + await walletDb.addParticipantIdentity(account, identity.serialize()) + + const storedIdentities = await AsyncUtils.materialize( + walletDb.getParticipantIdentities(account), + ) + + expect(storedIdentities.length).toEqual(1) + expect(storedIdentities[0]).toEqualBuffer(identity.serialize()) + }) + }) }) diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index 4e01e2f4e6..32fd40e64e 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -38,6 +38,7 @@ import { BalanceValue, BalanceValueEncoding } from './balanceValue' import { DecryptedNoteValue, DecryptedNoteValueEncoding } from './decryptedNoteValue' import { HeadValue, NullableHeadValueEncoding } from './headValue' import { AccountsDBMeta, MetaValue, MetaValueEncoding } from './metaValue' +import { ParticipantIdentity, ParticipantIdentityEncoding } from './participantIdentity' import { TransactionValue, TransactionValueEncoding } from './transactionValue' const VERSION_DATABASE_ACCOUNTS = 31 @@ -134,6 +135,16 @@ export class WalletDB { value: null }> + multisigSecrets: IDatabaseStore<{ + key: string + value: Buffer + }> + + participantIdentities: IDatabaseStore<{ + key: [Account['prefix'], Buffer] + value: ParticipantIdentity + }> + cacheStores: Array> constructor({ @@ -282,6 +293,22 @@ export class WalletDB { valueEncoding: NULL_ENCODING, }) + this.multisigSecrets = this.db.addStore({ + name: 'ms', + keyEncoding: new StringEncoding(), + valueEncoding: new BufferEncoding(), + }) + + this.participantIdentities = this.db.addStore({ + name: 'pi', + keyEncoding: new PrefixEncoding( + new BufferEncoding(), // account prefix + new BufferEncoding(), // participant identifier + 4, + ), + valueEncoding: new ParticipantIdentityEncoding(), + }) + // IDatabaseStores that cache and index decrypted chain data this.cacheStores = [ this.decryptedNotes, @@ -1242,4 +1269,55 @@ export class WalletDB { ): Promise { await this.nullifierToTransactionHash.del([account.prefix, nullifier], tx) } + + async putMultisigSecret( + name: string, + secret: Buffer, + tx?: IDatabaseTransaction, + ): Promise { + await this.multisigSecrets.put(name, secret, tx) + } + + async getMultisigSecret( + name: string, + tx?: IDatabaseTransaction, + ): Promise { + return this.multisigSecrets.get(name, tx) + } + + async hasMultisigSecret(name: string, tx?: IDatabaseTransaction): Promise { + return (await this.getMultisigSecret(name, tx)) !== undefined + } + + async deleteMultisigSecret(name: string, tx?: IDatabaseTransaction): Promise { + await this.multisigSecrets.del(name, tx) + } + + async addParticipantIdentity( + account: Account, + identity: Buffer, + tx?: IDatabaseTransaction, + ): Promise { + await this.participantIdentities.put([account.prefix, identity], { identity }, tx) + } + + async deleteParticipantIdentity( + account: Account, + identity: Buffer, + tx?: IDatabaseTransaction, + ): Promise { + await this.participantIdentities.del([account.prefix, identity], tx) + } + + async *getParticipantIdentities( + account: Account, + tx?: IDatabaseTransaction, + ): AsyncGenerator { + for await (const [_, identity] of this.participantIdentities.getAllKeysIter( + tx, + account.prefixRange, + )) { + yield identity + } + } }