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: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" } + ], + "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 + } + } }