From ffcde0e996bd2684a3f0aff51d8344ae9fc0bd54 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Wed, 17 Apr 2024 05:50:10 -0700 Subject: [PATCH 01/35] updating level db version to 6.1.1 (#4894) --- ironfish/package.json | 4 ++-- yarn.lock | 55 +++++++++++++++++++++++++++++++++---------- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/ironfish/package.json b/ironfish/package.json index 739c20178b..d078f320a0 100644 --- a/ironfish/package.json +++ b/ironfish/package.json @@ -38,7 +38,7 @@ "fastpriorityqueue": "0.7.1", "imurmurhash": "0.1.4", "level-errors": "2.0.1", - "leveldown": "5.6.0", + "leveldown": "6.1.1", "levelup": "4.4.0", "lodash": "4.17.21", "node-datachannel": "0.5.1", @@ -100,4 +100,4 @@ "url": "https://github.com/iron-fish/ironfish/issues" }, "homepage": "https://ironfish.network" -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 0e56281a86..ff467eaedb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4065,6 +4065,18 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +abstract-leveldown@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-7.2.0.tgz#08d19d4e26fb5be426f7a57004851b39e1795a2e" + integrity sha512-DnhQwcFEaYsvYDnACLZhMmCWd3rkOeEvglpa4q5i/5Jlm3UIsWaxVzuXvDLFCSCWRO3yy2/+V/G7FusFgejnfQ== + dependencies: + buffer "^6.0.3" + catering "^2.0.0" + is-buffer "^2.0.5" + level-concat-iterator "^3.0.0" + level-supports "^2.0.1" + queue-microtask "^1.2.3" + abstract-leveldown@~6.2.1: version "6.2.3" resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz#036543d87e3710f2528e47040bc3261b77a9a8eb" @@ -4558,7 +4570,7 @@ buffer@4.9.2: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@6.0.3: +buffer@6.0.3, buffer@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== @@ -4713,6 +4725,11 @@ cardinal@^2.1.1: ansicolors "~0.3.2" redeyed "~2.1.0" +catering@^2.0.0, catering@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/catering/-/catering-2.1.1.tgz#66acba06ed5ee28d5286133982a927de9a04b510" + integrity sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w== + chai@4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5" @@ -6881,6 +6898,11 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-buffer@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + is-callable@^1.1.4, is-callable@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" @@ -7822,6 +7844,13 @@ lerna@6.4.1: nx ">=15.4.2 < 16" typescript "^3 || ^4" +level-concat-iterator@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/level-concat-iterator/-/level-concat-iterator-3.1.0.tgz#5235b1f744bc34847ed65a50548aa88d22e881cf" + integrity sha512-BWRCMHBxbIqPxJ8vHOvKUsaO0v1sLYZtjN3K2iZJsRBYtp+ONsY6Jfi6hy9K3+zolgQRryhIn2NRZjZnWJ9NmQ== + dependencies: + catering "^2.1.0" + level-concat-iterator@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz#1d1009cf108340252cb38c51f9727311193e6263" @@ -7843,6 +7872,11 @@ level-iterator-stream@~4.0.0: readable-stream "^3.4.0" xtend "^4.0.2" +level-supports@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/level-supports/-/level-supports-2.1.0.tgz#9af908d853597ecd592293b2fad124375be79c5f" + integrity sha512-E486g1NCjW5cF78KGPrMDRBYzPuueMZ6VBXHT6gC7A8UYWGiM14fGgp+s/L1oFfDWSPV/+SFkYCmZ0SiESkRKA== + level-supports@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/level-supports/-/level-supports-1.0.1.tgz#2f530a596834c7301622521988e2c36bb77d122d" @@ -7850,14 +7884,14 @@ level-supports@~1.0.0: dependencies: xtend "^4.0.2" -leveldown@5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/leveldown/-/leveldown-5.6.0.tgz#16ba937bb2991c6094e13ac5a6898ee66d3eee98" - integrity sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ== +leveldown@6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/leveldown/-/leveldown-6.1.1.tgz#0f0e480fa88fd807abf94c33cb7e40966ea4b5ce" + integrity sha512-88c+E+Eizn4CkQOBHwqlCJaTNEjGpaEIikn1S+cINc5E9HEvJ77bqY4JY/HxT5u0caWqsc3P3DcFIKBI1vHt+A== dependencies: - abstract-leveldown "~6.2.1" + abstract-leveldown "^7.2.0" napi-macros "~2.0.0" - node-gyp-build "~4.1.0" + node-gyp-build "^4.3.0" levelup@4.4.0: version "4.4.0" @@ -8546,11 +8580,6 @@ node-gyp-build@^4.3.0: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40" integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg== -node-gyp-build@~4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.1.1.tgz#d7270b5d86717068d114cc57fff352f96d745feb" - integrity sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ== - node-gyp@8.4.1, node-gyp@8.x, node-gyp@^8.2.0: version "8.4.1" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" @@ -9539,7 +9568,7 @@ querystring@0.2.0: resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= -queue-microtask@^1.2.2: +queue-microtask@^1.2.2, queue-microtask@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== From 1ec3e61c0565abe4a6185622cadbead568df4af4 Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 17 Apr 2024 12:16:54 -0700 Subject: [PATCH 02/35] Update to the latest version of ironfish-frost --- Cargo.lock | 2 +- ironfish-rust-nodejs/src/multisig.rs | 19 ++++++------------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 23367fe0b4..e0db2d8363 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1521,7 +1521,7 @@ dependencies = [ [[package]] name = "ironfish-frost" version = "0.1.0" -source = "git+https://github.com/iron-fish/ironfish-frost.git?branch=main#b6b11147654602c0ea6142bb26b7df2b5651073f" +source = "git+https://github.com/iron-fish/ironfish-frost.git?branch=main#e432d1fc6f466b19cb01231ff20443d9bcb182a5" dependencies = [ "blake3", "chacha20 0.9.1", diff --git a/ironfish-rust-nodejs/src/multisig.rs b/ironfish-rust-nodejs/src/multisig.rs index e3acf0d6e1..2f3d7c6c17 100644 --- a/ironfish-rust-nodejs/src/multisig.rs +++ b/ironfish-rust-nodejs/src/multisig.rs @@ -11,11 +11,8 @@ use ironfish::{ SaplingKey, }; use ironfish_frost::{ - dkg::round1::{import_secret_package, PublicPackage}, - keys::PublicKeyPackage, - multienc, - nonces::deterministic_signing_nonces, - signature_share::SignatureShare, + dkg::round1::PublicPackage, keys::PublicKeyPackage, multienc, + nonces::deterministic_signing_nonces, signature_share::SignatureShare, signing_commitment::SigningCommitment, }; use napi::{bindgen_prelude::*, JsBuffer}; @@ -414,16 +411,12 @@ pub fn dkg_round2( ) -> Result { let secret = Secret::deserialize_from(&hex_to_vec_bytes(&secret).map_err(to_napi_err)?[..])?; let public_packages = try_deserialize_public_packages(public_packages)?; - - let secret_package = import_secret_package( - &hex_to_vec_bytes(&encrypted_secret_package).map_err(to_napi_err)?, - &secret, - ) - .map_err(to_napi_err)?; + let encrypted_secret_package = + hex_to_vec_bytes(&encrypted_secret_package).map_err(to_napi_err)?; let (encrypted_secret_package, public_packages) = ironfish_frost::dkg::round2::round2( - &secret.to_identity(), - &secret_package, + &secret, + &encrypted_secret_package, &public_packages, thread_rng(), ) From 60beae353c606f1044784510cacf8e3c4b67fa22 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Wed, 17 Apr 2024 17:03:03 -0400 Subject: [PATCH 03/35] fix(ironfish): Use CLI flag option for name (#4903) --- ironfish/src/rpc/routes/wallet/importAccount.test.ts | 5 +++-- ironfish/src/rpc/routes/wallet/importAccount.ts | 1 + ironfish/src/rpc/routes/wallet/utils.ts | 3 ++- ironfish/src/wallet/wallet.ts | 3 +-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ironfish/src/rpc/routes/wallet/importAccount.test.ts b/ironfish/src/rpc/routes/wallet/importAccount.test.ts index cc956f7869..642f78b6aa 100644 --- a/ironfish/src/rpc/routes/wallet/importAccount.test.ts +++ b/ironfish/src/rpc/routes/wallet/importAccount.test.ts @@ -390,13 +390,14 @@ describe('Route wallet/importAccount', () => { name: testCaseFile, }) + const name = 'new-account-name' const response = await routeTest.client.wallet.importAccount({ account: testCase, - name: testCaseFile, + name, }) expect(response.status).toBe(200) - expect(response.content.name).not.toBeNull() + expect(response.content.name).toEqual(name) } }) }) diff --git a/ironfish/src/rpc/routes/wallet/importAccount.ts b/ironfish/src/rpc/routes/wallet/importAccount.ts index 9972336561..4d5b4c72ee 100644 --- a/ironfish/src/rpc/routes/wallet/importAccount.ts +++ b/ironfish/src/rpc/routes/wallet/importAccount.ts @@ -57,6 +57,7 @@ routes.register( accountImport = await tryDecodeAccountWithMultisigSecrets( context.wallet, request.data.account, + { name }, ) } diff --git a/ironfish/src/rpc/routes/wallet/utils.ts b/ironfish/src/rpc/routes/wallet/utils.ts index f850eb25a0..fca6022e1b 100644 --- a/ironfish/src/rpc/routes/wallet/utils.ts +++ b/ironfish/src/rpc/routes/wallet/utils.ts @@ -284,12 +284,13 @@ export async function serializeRpcAccountStatus( export async function tryDecodeAccountWithMultisigSecrets( wallet: Wallet, value: string, + options?: { name?: string }, ): Promise { const encoder = new Base64JsonEncoder() for await (const { name, secret } of wallet.walletDb.getMultisigSecrets()) { try { - return encoder.decode(value, { name, multisigSecret: secret }) + return encoder.decode(value, { name: options?.name ?? name, multisigSecret: secret }) } catch (e: unknown) { continue } diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index 9fb9a1e2cb..ab77eaf905 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -1526,7 +1526,7 @@ export class Wallet { async importAccount(accountValue: AccountImport): Promise { let multisigKeys = accountValue.multisigKeys - let name = accountValue.name + const name = accountValue.name if ( accountValue.multisigKeys && @@ -1539,7 +1539,6 @@ export class Wallet { throw new Error('Cannot import identity without a corresponding multisig secret') } - name = multisigSecret.name multisigKeys = { keyPackage: accountValue.multisigKeys.keyPackage, publicKeyPackage: accountValue.multisigKeys.publicKeyPackage, From ed6f98fc0cd7c2cd1791edc9848acfcd392f900e Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 16 Apr 2024 09:21:58 -0700 Subject: [PATCH 04/35] Implement `wallet/multisig/dkg/round3` RPC --- ironfish-rust-nodejs/index.d.ts | 10 + ironfish-rust-nodejs/src/multisig.rs | 99 +++++--- ironfish-rust/src/frost_utils/account_keys.rs | 70 ++++++ ironfish-rust/src/frost_utils/mod.rs | 2 + ironfish/src/rpc/clients/client.ts | 9 + .../rpc/routes/wallet/multisig/dkg/index.ts | 1 + .../routes/wallet/multisig/dkg/round3.test.ts | 237 ++++++++++++++++++ .../rpc/routes/wallet/multisig/dkg/round3.ts | 93 +++++++ 8 files changed, 493 insertions(+), 28 deletions(-) create mode 100644 ironfish-rust/src/frost_utils/account_keys.rs create mode 100644 ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts create mode 100644 ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index 35812ac3d0..3b5b81ba62 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -264,6 +264,16 @@ export namespace multisig { encryptedSecretPackage: string publicPackages: Array } + export function dkgRound3(secret: ParticipantSecret, round2SecretPackage: string, round1PublicPackages: Array, round2PublicPackages: Array): DkgRound3Packages + export interface DkgRound3Packages { + publicAddress: string + keyPackage: string + publicKeyPackage: string + viewKey: string + incomingViewKey: string + outgoingViewKey: string + proofAuthorizingKey: string + } export function aggregateSignatureShares(publicKeyPackageStr: string, signingPackageStr: string, signatureSharesArr: Array): Buffer export class ParticipantSecret { constructor(jsBytes: Buffer) diff --git a/ironfish-rust-nodejs/src/multisig.rs b/ironfish-rust-nodejs/src/multisig.rs index 2f3d7c6c17..307d9d9f2f 100644 --- a/ironfish-rust-nodejs/src/multisig.rs +++ b/ironfish-rust-nodejs/src/multisig.rs @@ -5,19 +5,22 @@ use crate::{structs::NativeUnsignedTransaction, to_napi_err}; use ironfish::{ frost::{keys::KeyPackage, round2, Randomizer}, - frost_utils::{signing_package::SigningPackage, split_spender_key::split_spender_key}, + frost_utils::{ + account_keys::derive_account_keys, signing_package::SigningPackage, + split_spender_key::split_spender_key, + }, participant::{Identity, Secret}, serializing::{bytes_to_hex, fr::FrSerializable, hex_to_vec_bytes}, SaplingKey, }; use ironfish_frost::{ - dkg::round1::PublicPackage, keys::PublicKeyPackage, multienc, - nonces::deterministic_signing_nonces, signature_share::SignatureShare, - signing_commitment::SigningCommitment, + dkg, keys::PublicKeyPackage, multienc, nonces::deterministic_signing_nonces, + signature_share::SignatureShare, signing_commitment::SigningCommitment, }; use napi::{bindgen_prelude::*, JsBuffer}; use napi_derive::napi; use rand::thread_rng; +use std::io; use std::ops::Deref; #[napi(namespace = "multisig")] @@ -26,41 +29,32 @@ pub const IDENTITY_LEN: u32 = ironfish::frost_utils::IDENTITY_LEN as u32; #[napi(namespace = "multisig")] pub const SECRET_LEN: u32 = ironfish_frost::participant::SECRET_LEN as u32; -fn try_deserialize_identities(signers: I) -> Result> +fn try_deserialize(items: I, deserialize_item: F) -> Result> where I: IntoIterator, S: Deref, + F: for<'a> Fn(&'a [u8]) -> io::Result, { - signers + items .into_iter() - .try_fold(Vec::new(), |mut signers, serialized_identity| { - let serialized_identity = - hex_to_vec_bytes(&serialized_identity).map_err(to_napi_err)?; - Identity::deserialize_from(&serialized_identity[..]) - .map(|identity| { - signers.push(identity); - signers + .try_fold(Vec::new(), |mut items, serialized_item| { + let serialized_item = hex_to_vec_bytes(&serialized_item).map_err(to_napi_err)?; + deserialize_item(&serialized_item[..]) + .map(|item| { + items.push(item); + items }) .map_err(to_napi_err) }) } -fn try_deserialize_public_packages(public_packages: I) -> Result> +#[inline] +fn try_deserialize_identities(signers: I) -> Result> where I: IntoIterator, S: Deref, { - public_packages - .into_iter() - .try_fold(Vec::new(), |mut public_packages, serialized_package| { - let serialized_package = hex_to_vec_bytes(&serialized_package).map_err(to_napi_err)?; - PublicPackage::deserialize_from(&serialized_package[..]) - .map(|public_package| { - public_packages.push(public_package); - public_packages - }) - .map_err(to_napi_err) - }) + try_deserialize(signers, |bytes| Identity::deserialize_from(bytes)) } #[napi(namespace = "multisig")] @@ -383,7 +377,7 @@ pub fn dkg_round1( Identity::deserialize_from(&hex_to_vec_bytes(&self_identity).map_err(to_napi_err)?[..])?; let participant_identities = try_deserialize_identities(participant_identities)?; - let (encrypted_secret_package, public_package) = ironfish_frost::dkg::round1::round1( + let (encrypted_secret_package, public_package) = dkg::round1::round1( &self_identity, min_signers, &participant_identities, @@ -410,11 +404,13 @@ pub fn dkg_round2( public_packages: Vec, ) -> Result { let secret = Secret::deserialize_from(&hex_to_vec_bytes(&secret).map_err(to_napi_err)?[..])?; - let public_packages = try_deserialize_public_packages(public_packages)?; + let public_packages = try_deserialize(public_packages, |bytes| { + dkg::round1::PublicPackage::deserialize_from(bytes) + })?; let encrypted_secret_package = hex_to_vec_bytes(&encrypted_secret_package).map_err(to_napi_err)?; - let (encrypted_secret_package, public_packages) = ironfish_frost::dkg::round2::round2( + let (encrypted_secret_package, public_packages) = dkg::round2::round2( &secret, &encrypted_secret_package, &public_packages, @@ -447,3 +443,50 @@ pub struct DkgRound2Packages { pub encrypted_secret_package: String, pub public_packages: Vec, } + +#[napi(object, namespace = "multisig")] +pub fn dkg_round3( + secret: &ParticipantSecret, + round2_secret_package: String, + round1_public_packages: Vec, + round2_public_packages: Vec, +) -> Result { + let round2_secret_package = hex_to_vec_bytes(&round2_secret_package).map_err(to_napi_err)?; + let round1_public_packages = try_deserialize(round1_public_packages, |bytes| { + dkg::round1::PublicPackage::deserialize_from(bytes) + })?; + let round2_public_packages = try_deserialize(round2_public_packages, |bytes| { + dkg::round2::PublicPackage::deserialize_from(bytes) + })?; + + let (key_package, public_key_package, group_secret_key) = dkg::round3::round3( + &secret.secret, + &round2_secret_package, + round1_public_packages.iter(), + round2_public_packages.iter(), + ) + .map_err(to_napi_err)?; + + let account_keys = derive_account_keys(public_key_package.verifying_key(), &group_secret_key); + + Ok(DkgRound3Packages { + public_address: account_keys.public_address.hex_public_address(), + key_package: bytes_to_hex(&key_package.serialize().map_err(to_napi_err)?), + public_key_package: bytes_to_hex(&public_key_package.serialize()), + view_key: account_keys.view_key.hex_key(), + incoming_view_key: account_keys.incoming_viewing_key.hex_key(), + outgoing_view_key: account_keys.outgoing_viewing_key.hex_key(), + proof_authorizing_key: account_keys.proof_authorizing_key.hex_key(), + }) +} + +#[napi(object, namespace = "multisig")] +pub struct DkgRound3Packages { + pub public_address: String, + pub key_package: String, + pub public_key_package: String, + pub view_key: String, + pub incoming_view_key: String, + pub outgoing_view_key: String, + pub proof_authorizing_key: String, +} diff --git a/ironfish-rust/src/frost_utils/account_keys.rs b/ironfish-rust/src/frost_utils/account_keys.rs new file mode 100644 index 0000000000..bb5380ce39 --- /dev/null +++ b/ironfish-rust/src/frost_utils/account_keys.rs @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::{IncomingViewKey, OutgoingViewKey, PublicAddress, SaplingKey, ViewKey}; +use group::GroupEncoding; +use ironfish_frost::frost::VerifyingKey; +use ironfish_zkp::constants::PROOF_GENERATION_KEY_GENERATOR; +use jubjub::SubgroupPoint; + +pub struct MultisigAccountKeys { + /// Equivalent to [`crate::keys::SaplingKey::proof_authorizing_key`] + pub proof_authorizing_key: jubjub::Fr, + /// Equivalent to [`crate::keys::SaplingKey::outgoing_viewing_key`] + pub outgoing_viewing_key: OutgoingViewKey, + /// Equivalent to [`crate::keys::SaplingKey::view_key`] + pub view_key: ViewKey, + /// Equivalent to [`crate::keys::SaplingKey::incoming_viewing_key`] + pub incoming_viewing_key: IncomingViewKey, + /// Equivalent to [`crate::keys::SaplingKey::public_address`] + pub public_address: PublicAddress, +} + +/// Derives the account keys for a multisig account, realizing the following key hierarchy: +/// +/// ``` +/// ak ─┐ +/// ├─ ivk ── pk +/// gsk ── nsk ── nk ─┘ +/// ``` +pub fn derive_account_keys( + authorizing_key: &VerifyingKey, + group_secret_key: &[u8; 32], +) -> MultisigAccountKeys { + // Group secret key (gsk), obtained from the multisig setup process + let group_secret_key = + SaplingKey::new(*group_secret_key).expect("failed to derive group secret key"); + + // Authorization key (ak), obtained from the multisig setup process + let authorizing_key = Option::from(SubgroupPoint::from_bytes(&authorizing_key.serialize())) + .expect("failied to derive authorizing key"); + + // Nullifier keys (nsk and nk), derived from the gsk + let proof_authorizing_key = group_secret_key.sapling_proof_generation_key().nsk; + let nullifier_deriving_key = *PROOF_GENERATION_KEY_GENERATOR * proof_authorizing_key; + + // Incoming view key (ivk), derived from the ak and the nk + let view_key = ViewKey { + authorizing_key, + nullifier_deriving_key, + }; + let incoming_viewing_key = IncomingViewKey { + view_key: SaplingKey::hash_viewing_key(&authorizing_key, &nullifier_deriving_key) + .expect("failed to derive view key"), + }; + + // Outgoing view key (ovk), derived from the gsk + let outgoing_viewing_key = group_secret_key.outgoing_view_key().clone(); + + // Public address (pk), derived from the ivk + let public_address = incoming_viewing_key.public_address(); + + MultisigAccountKeys { + proof_authorizing_key, + outgoing_viewing_key, + view_key, + incoming_viewing_key, + public_address, + } +} diff --git a/ironfish-rust/src/frost_utils/mod.rs b/ironfish-rust/src/frost_utils/mod.rs index 707dcb945e..f35df12885 100644 --- a/ironfish-rust/src/frost_utils/mod.rs +++ b/ironfish-rust/src/frost_utils/mod.rs @@ -2,8 +2,10 @@ * 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/. */ +pub mod account_keys; 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 2e8abab174..6e683a7670 100644 --- a/ironfish/src/rpc/clients/client.ts +++ b/ironfish/src/rpc/clients/client.ts @@ -37,6 +37,8 @@ import type { DkgRound1Response, DkgRound2Request, DkgRound2Response, + DkgRound3Request, + DkgRound3Response, EstimateFeeRateRequest, EstimateFeeRateResponse, EstimateFeeRatesRequest, @@ -290,6 +292,13 @@ export abstract class RpcClient { params, ).waitForEnd() }, + + round3: (params: DkgRound3Request): Promise> => { + return this.request( + `${ApiNamespace.wallet}/multisig/dkg/round3`, + params, + ).waitForEnd() + }, }, }, diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/index.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/index.ts index eaa1f3a468..e1c096e1df 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/index.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/index.ts @@ -3,3 +3,4 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ export * from './round1' export * from './round2' +export * from './round3' diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts new file mode 100644 index 0000000000..371aff6694 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts @@ -0,0 +1,237 @@ +/* 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 { createRouteTest } from '../../../../../testUtilities/routeTest' + +function removeOneElement(array: Array): Array { + const newArray = [...array] + const removeIndex = Math.floor(Math.random() * array.length) + newArray.splice(removeIndex, 1) + return newArray +} + +describe('Route multisig/dkg/round3', () => { + const routeTest = createRouteTest() + + it('should create round 3 packages', async () => { + const secretNames = ['secret-0', 'secret-1', 'secret-2'] + + // Create participants and retrieve their identities + await Promise.all( + secretNames.map((name) => routeTest.client.wallet.multisig.createParticipant({ name })), + ) + const participants = await Promise.all( + secretNames.map( + async (name) => (await routeTest.client.wallet.multisig.getIdentity({ name })).content, + ), + ) + + // Perform DKG round 1 + const round1Packages = await Promise.all( + secretNames.map((secretName) => + routeTest.client.wallet.multisig.dkg.round1({ + secretName, + minSigners: 2, + participants, + }), + ), + ) + + // Perform DKG round 2 + const round2Packages = await Promise.all( + secretNames.map((secretName, index) => + routeTest.client.wallet.multisig.dkg.round2({ + secretName, + encryptedSecretPackage: round1Packages[index].content.encryptedSecretPackage, + publicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), + }), + ), + ) + + // Perform DKG round 3 + await Promise.all( + secretNames.map((secretName, index) => + routeTest.client.wallet.multisig.dkg.round3({ + secretName, + round2SecretPackage: round2Packages[index].content.encryptedSecretPackage, + round1PublicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), + round2PublicPackages: round2Packages.flatMap((pkg) => + pkg.content.publicPackages + .filter( + ({ recipientIdentity }) => recipientIdentity === participants[index].identity, + ) + .map(({ publicPackage }) => publicPackage), + ), + }), + ), + ) + + // Check that all accounts that got imported after round 3 have the same public address + const publicKeys = await Promise.all( + secretNames.map( + async (account) => + ( + await routeTest.client.wallet.getAccountPublicKey({ account }) + ).content.publicKey, + ), + ) + const expectedPublicKey = publicKeys[0] + for (const publicKey of publicKeys) { + expect(publicKey).toBe(expectedPublicKey) + } + }) + + it('should fail if not all round 1 packages are passed as an input', async () => { + const secretNames = ['secret-0', 'secret-1', 'secret-2'] + + // Create participants and retrieve their identities + await Promise.all( + secretNames.map((name) => routeTest.client.wallet.multisig.createParticipant({ name })), + ) + const participants = await Promise.all( + secretNames.map( + async (name) => (await routeTest.client.wallet.multisig.getIdentity({ name })).content, + ), + ) + + // Perform DKG round 1 + const round1Packages = await Promise.all( + secretNames.map((secretName) => + routeTest.client.wallet.multisig.dkg.round1({ + secretName, + minSigners: 2, + participants, + }), + ), + ) + + // Perform DKG round 2 + const round2Packages = await Promise.all( + secretNames.map((secretName, index) => + routeTest.client.wallet.multisig.dkg.round2({ + secretName, + encryptedSecretPackage: round1Packages[index].content.encryptedSecretPackage, + publicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), + }), + ), + ) + + // Perform DKG round 3 + await expect( + routeTest.client.wallet.multisig.dkg.round3({ + secretName: secretNames[0], + round2SecretPackage: round2Packages[0].content.encryptedSecretPackage, + round1PublicPackages: removeOneElement( + round1Packages.map((pkg) => pkg.content.publicPackage), + ), + round2PublicPackages: round2Packages.flatMap((pkg) => + pkg.content.publicPackages + .filter(({ recipientIdentity }) => recipientIdentity === participants[0].identity) + .map(({ publicPackage }) => publicPackage), + ), + }), + ).rejects.toThrow('invalid input: expected 3 round 1 public packages, got 2') + }) + + it('should fail if not all round 2 packages are passed as an input', async () => { + const secretNames = ['secret-0', 'secret-1', 'secret-2'] + + // Create participants and retrieve their identities + await Promise.all( + secretNames.map((name) => routeTest.client.wallet.multisig.createParticipant({ name })), + ) + const participants = await Promise.all( + secretNames.map( + async (name) => (await routeTest.client.wallet.multisig.getIdentity({ name })).content, + ), + ) + + // Perform DKG round 1 + const round1Packages = await Promise.all( + secretNames.map((secretName) => + routeTest.client.wallet.multisig.dkg.round1({ + secretName, + minSigners: 2, + participants, + }), + ), + ) + + // Perform DKG round 2 + const round2Packages = await Promise.all( + secretNames.map((secretName, index) => + routeTest.client.wallet.multisig.dkg.round2({ + secretName, + encryptedSecretPackage: round1Packages[index].content.encryptedSecretPackage, + publicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), + }), + ), + ) + + // Perform DKG round 3 + await expect( + routeTest.client.wallet.multisig.dkg.round3({ + secretName: secretNames[0], + round2SecretPackage: round2Packages[0].content.encryptedSecretPackage, + round1PublicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), + round2PublicPackages: removeOneElement( + round2Packages.flatMap((pkg) => + pkg.content.publicPackages + .filter(({ recipientIdentity }) => recipientIdentity === participants[0].identity) + .map(({ publicPackage }) => publicPackage), + ), + ), + }), + ).rejects.toThrow('invalid input: expected 2 round 2 public packages, got 1') + }) + + it('should fail passing the wrong round 2 secret package', async () => { + const secretNames = ['secret-0', 'secret-1', 'secret-2'] + + // Create participants and retrieve their identities + await Promise.all( + secretNames.map((name) => routeTest.client.wallet.multisig.createParticipant({ name })), + ) + const participants = await Promise.all( + secretNames.map( + async (name) => (await routeTest.client.wallet.multisig.getIdentity({ name })).content, + ), + ) + + // Perform DKG round 1 + const round1Packages = await Promise.all( + secretNames.map((secretName) => + routeTest.client.wallet.multisig.dkg.round1({ + secretName, + minSigners: 2, + participants, + }), + ), + ) + + // Perform DKG round 2 + const round2Packages = await Promise.all( + secretNames.map((secretName, index) => + routeTest.client.wallet.multisig.dkg.round2({ + secretName, + encryptedSecretPackage: round1Packages[index].content.encryptedSecretPackage, + publicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), + }), + ), + ) + + // Perform DKG round 3 + await expect( + routeTest.client.wallet.multisig.dkg.round3({ + secretName: secretNames[0], + round2SecretPackage: round2Packages[1].content.encryptedSecretPackage, + round1PublicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), + round2PublicPackages: round2Packages.flatMap((pkg) => + pkg.content.publicPackages + .filter(({ recipientIdentity }) => recipientIdentity === participants[0].identity) + .map(({ publicPackage }) => publicPackage), + ), + }), + ).rejects.toThrow('decryption error: aead::Error') + }) +}) diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts new file mode 100644 index 0000000000..dddcc52126 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts @@ -0,0 +1,93 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { multisig } from '@ironfish/rust-nodejs' +import * as yup from 'yup' +import { Assert } from '../../../../../assert' +import { FullNode } from '../../../../../node' +import { ACCOUNT_SCHEMA_VERSION } from '../../../../../wallet' +import { RPC_ERROR_CODES, RpcValidationError } from '../../../../adapters' +import { ApiNamespace } from '../../../namespaces' +import { routes } from '../../../router' + +export type DkgRound3Request = { + secretName: string + round2SecretPackage: string + round1PublicPackages: Array + round2PublicPackages: Array +} + +export type DkgRound3Response = Record + +export const DkgRound3RequestSchema: yup.ObjectSchema = yup + .object({ + secretName: yup.string().defined(), + round2SecretPackage: yup.string().defined(), + round1PublicPackages: yup.array().of(yup.string().defined()).defined(), + round2PublicPackages: yup.array().of(yup.string().defined()).defined(), + }) + .defined() + +export const DkgRound3ResponseSchema: yup.ObjectSchema = yup + .object>({}) + .defined() + +routes.register( + `${ApiNamespace.wallet}/multisig/dkg/round3`, + DkgRound3RequestSchema, + async (request, node): Promise => { + Assert.isInstanceOf(node, FullNode) + + const { secretName } = request.data + const multisigSecret = await node.wallet.walletDb.getMultisigSecretByName(secretName) + + if (!multisigSecret) { + throw new RpcValidationError( + `Multisig secret with name '${secretName}' not found`, + 400, + RPC_ERROR_CODES.MULTISIG_SECRET_NOT_FOUND, + ) + } + + const secret = new multisig.ParticipantSecret(multisigSecret.secret) + const identity = secret.toIdentity().serialize().toString('hex') + + const { + publicAddress, + keyPackage, + publicKeyPackage, + viewKey, + incomingViewKey, + outgoingViewKey, + proofAuthorizingKey, + } = multisig.dkgRound3( + secret, + request.data.round2SecretPackage, + request.data.round1PublicPackages, + request.data.round2PublicPackages, + ) + + const accountImport = { + name: secretName, + version: ACCOUNT_SCHEMA_VERSION, + createdAt: null, + spendingKey: null, + viewKey, + incomingViewKey, + outgoingViewKey, + publicAddress, + proofAuthorizingKey, + multisigKeys: { + identity, + keyPackage, + publicKeyPackage, + }, + } + + await node.wallet.importAccount(accountImport) + + // TODO: add an option to skip rescan + + request.end({}) + }, +) From 522cd38639bc998f8a6d04602e47be59aa460391 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Thu, 18 Apr 2024 12:00:15 -0400 Subject: [PATCH 05/35] feat(cli,ironfish): Add `wallet:multisig:dkg:round3` (#4905) --- .../commands/wallet/multisig/dkg/round3.ts | 101 ++++++++++++++++++ .../routes/wallet/multisig/dkg/round3.test.ts | 8 +- .../rpc/routes/wallet/multisig/dkg/round3.ts | 17 ++- 3 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts new file mode 100644 index 0000000000..84257e52f7 --- /dev/null +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -0,0 +1,101 @@ +/* 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 { CliUx, Flags } from '@oclif/core' +import { IronfishCommand } from '../../../../command' +import { RemoteFlags } from '../../../../flags' +import { longPrompt } from '../../../../utils/longPrompt' + +export class DkgRound3Command extends IronfishCommand { + static description = 'Perform round3 of the DKG protocol for multisig account creation' + static hidden = true + + static flags = { + ...RemoteFlags, + secretName: Flags.string({ + char: 's', + description: 'The name of the secret to use for decryption during DKG', + required: true, + }), + round2SecretPackage: Flags.string({ + char: 'e', + description: 'The encrypted secret package created during DKG round2', + }), + round1PublicPackages: Flags.string({ + char: 'p', + description: + 'The public package that a participant generated during DKG round1 (may be specified multiple times for multiple participants). Must include your own round1 public package', + multiple: true, + }), + round2PublicPackages: Flags.string({ + char: 'q', + description: + 'The public package that a participant generated during DKG round2 where the recipient matches the identity associated with the secret', + multiple: true, + }), + } + + async start(): Promise { + const { flags } = await this.parse(DkgRound3Command) + + let round2SecretPackage = flags.round2SecretPackage + if (!round2SecretPackage) { + round2SecretPackage = await CliUx.ux.prompt( + `Enter the encrypted secret package for secret ${flags.secretName}`, + { + required: true, + }, + ) + } + + let round1PublicPackages = flags.round1PublicPackages + if (!round1PublicPackages || round1PublicPackages.length < 2) { + const input = await longPrompt( + 'Enter public packages separated by commas, one for each participant', + { + required: true, + }, + ) + round1PublicPackages = input.split(',') + + if (round1PublicPackages.length < 2) { + this.error( + 'Must include a public package for each participant; at least 2 participants required', + ) + } + } + round1PublicPackages = round1PublicPackages.map((i) => i.trim()) + + let round2PublicPackages = flags.round2PublicPackages + if (!round2PublicPackages) { + const input = await longPrompt( + 'Enter public packages separated by commas, one for each participant', + { + required: true, + }, + ) + round2PublicPackages = input.split(',') + + if (round2PublicPackages.length !== round1PublicPackages.length - 1) { + this.error( + 'The number of round 2 public packages must be 1 less than the number of round 1 public packages', + ) + } + } + round2PublicPackages = round2PublicPackages.map((i) => i.trim()) + + const client = await this.sdk.connectRpc() + + const response = await client.wallet.multisig.dkg.round3({ + secretName: flags.secretName, + round2SecretPackage, + round1PublicPackages, + round2PublicPackages, + }) + + this.log() + this.log( + `Account ${response.content.name} imported with public address: ${response.content.publicAddress}`, + ) + } +} diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts index 371aff6694..678b84cbca 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts @@ -49,7 +49,7 @@ describe('Route multisig/dkg/round3', () => { ) // Perform DKG round 3 - await Promise.all( + const round3Responses = await Promise.all( secretNames.map((secretName, index) => routeTest.client.wallet.multisig.dkg.round3({ secretName, @@ -79,6 +79,12 @@ describe('Route multisig/dkg/round3', () => { for (const publicKey of publicKeys) { expect(publicKey).toBe(expectedPublicKey) } + + // Check all the responses match + expect(round3Responses).toHaveLength(publicKeys.length) + for (let i = 0; i < round3Responses.length; i++) { + expect(round3Responses[i].content.publicAddress).toEqual(publicKeys[i]) + } }) it('should fail if not all round 1 packages are passed as an input', async () => { diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts index dddcc52126..ce97b13a27 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts @@ -17,7 +17,10 @@ export type DkgRound3Request = { round2PublicPackages: Array } -export type DkgRound3Response = Record +export type DkgRound3Response = { + name: string + publicAddress: string +} export const DkgRound3RequestSchema: yup.ObjectSchema = yup .object({ @@ -29,7 +32,10 @@ export const DkgRound3RequestSchema: yup.ObjectSchema = yup .defined() export const DkgRound3ResponseSchema: yup.ObjectSchema = yup - .object>({}) + .object({ + name: yup.string().defined(), + publicAddress: yup.string().defined(), + }) .defined() routes.register( @@ -84,10 +90,13 @@ routes.register( }, } - await node.wallet.importAccount(accountImport) + const account = await node.wallet.importAccount(accountImport) // TODO: add an option to skip rescan - request.end({}) + request.end({ + name: account.name, + publicAddress: account.publicAddress, + }) }, ) From b3a2e428cbab3d8c51f98e5e8bdeee102a4c4652 Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 17 Apr 2024 13:03:06 -0700 Subject: [PATCH 06/35] Update Cargo dependencies to fix dependabot alerts Fixes: - https://github.com/iron-fish/ironfish/security/dependabot/76 (openssl) - https://github.com/iron-fish/ironfish/security/dependabot/81 (h2) - https://github.com/iron-fish/ironfish/security/dependabot/91 (mio) - https://github.com/iron-fish/ironfish/security/dependabot/93 (h2) --- Cargo.lock | 44 +++++++++++++++++++++++++++++---------- supply-chain/audits.toml | 30 ++++++++++++++++++++++++++ supply-chain/imports.lock | 35 +++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e0db2d8363..4c83ce7816 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -510,7 +510,7 @@ checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "bitflags 1.3.2", "clap_lex", - "indexmap", + "indexmap 1.9.3", "textwrap", ] @@ -919,6 +919,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.1" @@ -1223,9 +1229,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.19" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -1233,7 +1239,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -1294,6 +1300,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + [[package]] name = "heapless" version = "0.7.0" @@ -1471,7 +1483,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", ] [[package]] @@ -1774,9 +1796,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mio" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", @@ -1924,9 +1946,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.59" +version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a257ad03cd8fb16ad4172fedf8094451e1af1c4b70097636ef2eac9a5f0cc33" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ "bitflags 2.3.3", "cfg-if", @@ -1956,9 +1978,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.95" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml index 469bb3fb26..3d6680db9a 100644 --- a/supply-chain/audits.toml +++ b/supply-chain/audits.toml @@ -40,12 +40,42 @@ who = "Andrea " criteria = "safe-to-deploy" version = "1.0.0" +[[audits.h2]] +who = "Andrea " +criteria = "safe-to-deploy" +delta = "0.4.0 -> 0.3.26" + +[[audits.hashbrown]] +who = "Andrea " +criteria = "safe-to-deploy" +delta = "0.14.0 -> 0.14.3" + +[[audits.indexmap]] +who = "Andrea " +criteria = "safe-to-deploy" +delta = "1.9.3 -> 2.2.6" + [[audits.jubjub]] who = "Andrea " criteria = "safe-to-deploy" delta = "0.9.0 -> 0.9.0@git:a1a0c2ed69eec4d5d5e87842e2a40849f7fa4633" notes = "Fork of the official jubjub owned by Iron Fish" +[[audits.mio]] +who = "Andrea " +criteria = "safe-to-deploy" +delta = "0.8.8 -> 0.8.11" + +[[audits.openssl]] +who = "Andrea " +criteria = "safe-to-deploy" +delta = "0.10.59 -> 0.10.64" + +[[audits.openssl-sys]] +who = "Andrea " +criteria = "safe-to-deploy" +delta = "0.9.95 -> 0.9.102" + [[audits.reddsa]] who = "Andrea " criteria = "safe-to-deploy" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index f587186fd7..26bc4e7d28 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -209,6 +209,24 @@ who = "Pat Hickey " criteria = "safe-to-deploy" version = "0.3.27" +[[audits.bytecode-alliance.audits.h2]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.3.19 -> 0.4.0" +notes = "A number of changes but nothing adding new `unsafe` or anything outside the purview of what this crate already manages." + +[[audits.bytecode-alliance.audits.hashbrown]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +delta = "0.12.3 -> 0.13.1" +notes = "The diff looks plausible. Much of it is low-level memory-layout code and I can't be 100% certain without a deeper dive into the implementation logic, but nothing looks actively malicious." + +[[audits.bytecode-alliance.audits.hashbrown]] +who = "Trevor Elliott " +criteria = "safe-to-deploy" +delta = "0.13.1 -> 0.13.2" +notes = "I read through the diff between v0.13.1 and v0.13.2, and verified that the changes made matched up with the changelog entries. There were very few changes between these two releases, and it was easy to verify what they did." + [[audits.bytecode-alliance.audits.httpdate]] who = "Pat Hickey " criteria = "safe-to-deploy" @@ -383,6 +401,12 @@ criteria = "safe-to-deploy" version = "0.2.7" aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +[[audits.google.audits.equivalent]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.0.1" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + [[audits.google.audits.fastrand]] who = "George Burgess IV " criteria = "safe-to-deploy" @@ -1066,6 +1090,17 @@ criteria = "safe-to-deploy" delta = "0.12.1 -> 0.13.0" aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" +[[audits.zcash.audits.hashbrown]] +who = "Daira Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.13.2 -> 0.14.0" +notes = """ +There is some additional use of unsafe code but the changes in this crate looked plausible. +There is a new default dependency on the `allocator-api2` crate, which itself has quite a lot of unsafe code. +Many previously undocumented safety requirements have been documented. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + [[audits.zcash.audits.inout]] who = "Daira Hopwood " criteria = "safe-to-deploy" From 5451d00d9508282342665711c13954f39bcc1e0a Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Thu, 18 Apr 2024 09:19:55 -0700 Subject: [PATCH 07/35] add option to force full account rescan from genesis (#4901) resets account createdAt to null on all accounts if the 'full' option is set on requests to wallet/rescanAccount. this forces rescans for all accounts to start at the genesis block instead of beginning at the account birthday. adds '--full' flag to wallet:rescan command. --- ironfish-cli/src/commands/wallet/rescan.ts | 7 +++++-- .../rpc/routes/wallet/rescanAccount.test.ts | 21 +++++++++++++++++++ .../src/rpc/routes/wallet/rescanAccount.ts | 5 +++-- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/rescan.ts b/ironfish-cli/src/commands/wallet/rescan.ts index 6ad02320f8..46487b05bd 100644 --- a/ironfish-cli/src/commands/wallet/rescan.ts +++ b/ironfish-cli/src/commands/wallet/rescan.ts @@ -28,11 +28,14 @@ export class RescanCommand extends IronfishCommand { description: 'Sequence to start account rescan from', hidden: true, }), + full: Flags.boolean({ + description: 'Force a full rescan of the chain starting from the genesis block', + }), } async start(): Promise { const { flags } = await this.parse(RescanCommand) - const { follow, local, from } = flags + const { follow, local, from, full } = flags if (local && !follow) { this.error('You cannot pass both --local and --no-follow') @@ -44,7 +47,7 @@ export class RescanCommand extends IronfishCommand { stdout: true, }) - const response = client.wallet.rescanAccountStream({ follow, from }) + const response = client.wallet.rescanAccountStream({ follow, from, full }) const speed = new Meter() diff --git a/ironfish/src/rpc/routes/wallet/rescanAccount.test.ts b/ironfish/src/rpc/routes/wallet/rescanAccount.test.ts index 7cdde7f29c..04249de12c 100644 --- a/ironfish/src/rpc/routes/wallet/rescanAccount.test.ts +++ b/ironfish/src/rpc/routes/wallet/rescanAccount.test.ts @@ -149,4 +149,25 @@ describe('Route wallet/rescanAccount', () => { expect(updateHead).not.toHaveBeenCalled() expect(scanTransactions).toHaveBeenCalledTimes(1) }) + + it('resets createdAt on accounts on full rescans', async () => { + const chain = routeTest.node.chain + + let accountReloaded = routeTest.node.wallet.getAccountByName(account.name) + expect(accountReloaded).toBeDefined() + expect(accountReloaded?.createdAt?.hash).toEqualHash(chain.genesis.hash) + + jest.spyOn(routeTest.node.wallet, 'scanTransactions').mockReturnValue(Promise.resolve()) + + await routeTest.client + .request('wallet/rescanAccount', { + follow: false, + full: true, + }) + .waitForEnd() + + accountReloaded = routeTest.node.wallet.getAccountByName(account.name) + expect(accountReloaded).toBeDefined() + expect(accountReloaded?.createdAt).toBeNull() + }) }) diff --git a/ironfish/src/rpc/routes/wallet/rescanAccount.ts b/ironfish/src/rpc/routes/wallet/rescanAccount.ts index 4bfa80ca25..b05d28e4b0 100644 --- a/ironfish/src/rpc/routes/wallet/rescanAccount.ts +++ b/ironfish/src/rpc/routes/wallet/rescanAccount.ts @@ -8,13 +8,14 @@ import { ApiNamespace } from '../namespaces' import { routes } from '../router' import { AssertHasRpcContext } from '../rpcContext' -export type RescanAccountRequest = { follow?: boolean; from?: number } +export type RescanAccountRequest = { follow?: boolean; from?: number; full?: boolean } export type RescanAccountResponse = { sequence: number; startedAt: number; endSequence: number } export const RescanAccountRequestSchema: yup.ObjectSchema = yup .object({ follow: yup.boolean().optional(), from: yup.number().optional(), + full: yup.boolean().optional(), }) .defined() @@ -43,7 +44,7 @@ routes.register( await context.wallet.updateHeadState.abort() } - await context.wallet.reset() + await context.wallet.reset({ resetCreatedAt: request.data.full }) let fromHash = undefined if (request.data.from && request.data.from > GENESIS_BLOCK_SEQUENCE) { From 0fcacc9ad3d9c1f8c427b109ce2feca09048a1c2 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Thu, 18 Apr 2024 14:28:57 -0500 Subject: [PATCH 08/35] Rahul/ifl 2471 normalize confirm message in cli in a helper function (#4908) * Normalize confirm message in CLI in a helper function https://linear.app/if-labs/issue/IFL-2471/normalize-confirm-message-in-cli-in-a-helper-function * adds aborted messages --- ironfish-cli/src/commands/reset.ts | 12 ++--- ironfish-cli/src/commands/wallet/burn.ts | 17 +++---- ironfish-cli/src/commands/wallet/mint.ts | 48 ++++++++++--------- .../src/commands/wallet/notes/combine.ts | 11 ++--- ironfish-cli/src/commands/wallet/send.ts | 11 ++--- ironfish-cli/src/utils/confirm.ts | 23 +++++++++ ironfish-cli/src/utils/index.ts | 1 + 7 files changed, 75 insertions(+), 48 deletions(-) create mode 100644 ironfish-cli/src/utils/confirm.ts diff --git a/ironfish-cli/src/commands/reset.ts b/ironfish-cli/src/commands/reset.ts index 8ffcd953af..4a3cbf771e 100644 --- a/ironfish-cli/src/commands/reset.ts +++ b/ironfish-cli/src/commands/reset.ts @@ -13,6 +13,7 @@ import { VerboseFlag, VerboseFlagKey, } from '../flags' +import { confirmOperation } from '../utils' export default class Reset extends IronfishCommand { static description = 'Reset the node to its initial state' @@ -60,12 +61,11 @@ export default class Reset extends IronfishCommand { networkIdMessage + `\n\nAre you sure? (Y)es / (N)o` - const confirmed = flags.confirm || (await CliUx.ux.confirm(message)) - - if (!confirmed) { - this.log('Reset aborted.') - this.exit(0) - } + await confirmOperation({ + confirm: flags.confirm, + confirmMessage: message, + cancelledMessage: 'Reset aborted.', + }) CliUx.ux.action.start('Deleting databases...') diff --git a/ironfish-cli/src/commands/wallet/burn.ts b/ironfish-cli/src/commands/wallet/burn.ts index 43fd6a1f70..5dc00e2a1e 100644 --- a/ironfish-cli/src/commands/wallet/burn.ts +++ b/ironfish-cli/src/commands/wallet/burn.ts @@ -13,6 +13,7 @@ import { import { CliUx, Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { IronFlag, RemoteFlags, ValueFlag } from '../../flags' +import { confirmOperation } from '../../utils' import { selectAsset } from '../../utils/asset' import { promptCurrency } from '../../utils/currency' import { getExplorer } from '../../utils/explorer' @@ -207,9 +208,7 @@ export class Burn extends IronfishCommand { this.exit(0) } - if (!flags.confirm && !(await this.confirm(assetData, amount, raw.fee, account))) { - this.error('Transaction aborted.') - } + await this.confirm(assetData, amount, raw.fee, account, flags.confirm) CliUx.ux.action.start('Sending the transaction') @@ -273,13 +272,15 @@ export class Burn extends IronfishCommand { amount: bigint, fee: bigint, account: string, - ): Promise { + confirm?: boolean, + ): Promise { const renderedAmount = CurrencyUtils.render(amount, true, asset.id, asset.verification) const renderedFee = CurrencyUtils.render(fee, true) - this.log( - `You are about to burn: ${renderedAmount} plus a transaction fee of ${renderedFee} with the account ${account}`, - ) - return CliUx.ux.confirm('Do you confirm (Y/N)?') + await confirmOperation({ + confirm, + confirmMessage: `You are about to burn: ${renderedAmount} plus a transaction fee of ${renderedFee} with the account ${account}\nDo you confirm(Y/N)?`, + cancelledMessage: 'Burn aborted.', + }) } } diff --git a/ironfish-cli/src/commands/wallet/mint.ts b/ironfish-cli/src/commands/wallet/mint.ts index b524ea4847..ea283d588c 100644 --- a/ironfish-cli/src/commands/wallet/mint.ts +++ b/ironfish-cli/src/commands/wallet/mint.ts @@ -17,6 +17,7 @@ import { import { CliUx, Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { IronFlag, RemoteFlags, ValueFlag } from '../../flags' +import { confirmOperation } from '../../utils' import { selectAsset } from '../../utils/asset' import { promptCurrency } from '../../utils/currency' import { getExplorer } from '../../utils/explorer' @@ -277,21 +278,17 @@ export class Mint extends IronfishCommand { this.exit(0) } - if ( - !flags.confirm && - !(await this.confirm( - account, - amount, - raw.fee, - assetId, - name, - metadata, - flags.transferOwnershipTo, - assetData, - )) - ) { - this.error('Transaction aborted.') - } + await this.confirm( + account, + amount, + raw.fee, + assetId, + name, + metadata, + flags.transferOwnershipTo, + flags.confirm, + assetData, + ) CliUx.ux.action.start('Sending the transaction') @@ -359,8 +356,9 @@ export class Mint extends IronfishCommand { name?: string, metadata?: string, transferOwnershipTo?: string, + confirm?: boolean, assetData?: RpcAsset, - ): Promise { + ): Promise { const nameString = name ? `\nName: ${name}` : '' const metadataString = metadata ? `\nMetadata: ${metadata}` : '' @@ -372,18 +370,24 @@ export class Mint extends IronfishCommand { ) const renderedFee = CurrencyUtils.render(fee, true) - this.log( + const confirmMessage = [ `You are about to mint an asset with the account ${account}:${nameString}${metadataString}`, - ) - this.log(`Amount: ${renderedAmount}`) - this.log(`Fee: ${renderedFee}`) + `Amount: ${renderedAmount}`, + `Fee: ${renderedFee}`, + ] if (transferOwnershipTo) { - this.log( + confirmMessage.push( `Ownership of this asset will be transferred to ${transferOwnershipTo}. The current account will no longer have any permission to mint or modify this asset. This cannot be undone.`, ) } - return CliUx.ux.confirm('Do you confirm (Y/N)?') + confirmMessage.push('Do you confirm (Y/N)?') + + await confirmOperation({ + confirmMessage: confirmMessage.join('\n'), + cancelledMessage: 'Mint aborted.', + confirm, + }) } } diff --git a/ironfish-cli/src/commands/wallet/notes/combine.ts b/ironfish-cli/src/commands/wallet/notes/combine.ts index 6b701ba800..fdfc01b309 100644 --- a/ironfish-cli/src/commands/wallet/notes/combine.ts +++ b/ironfish-cli/src/commands/wallet/notes/combine.ts @@ -16,6 +16,7 @@ import { CliUx, Flags } from '@oclif/core' import inquirer from 'inquirer' import { IronfishCommand } from '../../../command' import { IronFlag, RemoteFlags } from '../../../flags' +import { confirmOperation } from '../../../utils' import { getExplorer } from '../../../utils/explorer' import { selectFee } from '../../../utils/fees' import { fetchNotes } from '../../../utils/note' @@ -326,12 +327,10 @@ export class CombineNotesCommand extends IronfishCommand { })}`, ) - if (!flags.confirm) { - const confirmed = await CliUx.ux.confirm('Do you confirm (Y/N)?') - if (!confirmed) { - this.error('Transaction aborted.') - } - } + await confirmOperation({ + confirm: flags.confirm, + cancelledMessage: 'Combine aborted.', + }) transactionTimer.start() diff --git a/ironfish-cli/src/commands/wallet/send.ts b/ironfish-cli/src/commands/wallet/send.ts index b7cee6dd41..0dcd546639 100644 --- a/ironfish-cli/src/commands/wallet/send.ts +++ b/ironfish-cli/src/commands/wallet/send.ts @@ -14,6 +14,7 @@ import { import { CliUx, Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { HexFlag, IronFlag, RemoteFlags, ValueFlag } from '../../flags' +import { confirmOperation } from '../../utils' import { selectAsset } from '../../utils/asset' import { promptCurrency } from '../../utils/currency' import { getExplorer } from '../../utils/explorer' @@ -280,12 +281,10 @@ export class Send extends IronfishCommand { ) } - if (!flags.confirm) { - const confirmed = await CliUx.ux.confirm('Do you confirm (Y/N)?') - if (!confirmed) { - this.error('Transaction aborted.') - } - } + await confirmOperation({ + confirm: flags.confirm, + cancelledMessage: 'Transaction aborted.', + }) transactionTimer.start() diff --git a/ironfish-cli/src/utils/confirm.ts b/ironfish-cli/src/utils/confirm.ts new file mode 100644 index 0000000000..aeceb91e85 --- /dev/null +++ b/ironfish-cli/src/utils/confirm.ts @@ -0,0 +1,23 @@ +/* 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 { CliUx } from '@oclif/core' + +export const confirmOperation = async (options: { + confirmMessage?: string + cancelledMessage?: string + confirm?: boolean +}) => { + const { confirmMessage, cancelledMessage, confirm } = options + + if (confirm) { + return true + } + + const confirmed = await CliUx.ux.confirm(confirmMessage || 'Do you confirm (Y/N)?') + + if (!confirmed) { + CliUx.ux.log(cancelledMessage || 'Operation aborted.') + CliUx.ux.exit(0) + } +} diff --git a/ironfish-cli/src/utils/index.ts b/ironfish-cli/src/utils/index.ts index f6cc7d7bbf..ef7c5e32c5 100644 --- a/ironfish-cli/src/utils/index.ts +++ b/ironfish-cli/src/utils/index.ts @@ -3,6 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ export * from './asset' +export * from './confirm' export * from './editor' export * from './platform' export * from './rpc' From 1735b1f60fab889c93e432701f2fa9288aec990e Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 18 Apr 2024 08:59:08 -0700 Subject: [PATCH 09/35] Always honor the `name` parameter passed to `importAccount` When calling `importAccount` with an `RpcAccountImport` and a non-undefined `name`, the `name` parameter used to be ignored. This is no longer the case: passing a non-undefined `name` now overrides the name of the `RpcAccountImport`. --- .../rpc/routes/wallet/importAccount.test.ts | 28 +++++++++++++++++++ .../src/rpc/routes/wallet/importAccount.ts | 3 ++ 2 files changed, 31 insertions(+) diff --git a/ironfish/src/rpc/routes/wallet/importAccount.test.ts b/ironfish/src/rpc/routes/wallet/importAccount.test.ts index 642f78b6aa..dcdd6964a0 100644 --- a/ironfish/src/rpc/routes/wallet/importAccount.test.ts +++ b/ironfish/src/rpc/routes/wallet/importAccount.test.ts @@ -102,6 +102,34 @@ describe('Route wallet/importAccount', () => { }) }) + it('should import a spending account with the specified name', async () => { + const key = generateKey() + + const accountName = 'bar' + const overriddenAccountName = 'not-bar' + const response = await routeTest.client.wallet.importAccount({ + account: { + name: accountName, + viewKey: key.viewKey, + spendingKey: key.spendingKey, + publicAddress: key.publicAddress, + incomingViewKey: key.incomingViewKey, + outgoingViewKey: key.outgoingViewKey, + proofAuthorizingKey: null, + version: 1, + createdAt: null, + }, + name: overriddenAccountName, + rescan: false, + }) + + expect(response.status).toBe(200) + expect(response.content).toMatchObject({ + name: overriddenAccountName, + isDefaultAccount: false, // This is false because the default account is already imported in a previous test + }) + }) + describe('import rescanning', () => { let nodeClient: RpcClient | null = null diff --git a/ironfish/src/rpc/routes/wallet/importAccount.ts b/ironfish/src/rpc/routes/wallet/importAccount.ts index 4d5b4c72ee..da4eff4d3e 100644 --- a/ironfish/src/rpc/routes/wallet/importAccount.ts +++ b/ironfish/src/rpc/routes/wallet/importAccount.ts @@ -66,6 +66,9 @@ routes.register( } } else { accountImport = deserializeRpcAccountImport(request.data.account) + if (request.data.name) { + accountImport.name = request.data.name + } } account = await context.wallet.importAccount(accountImport) From de196dba27c8a2c471e904f74f80191dbd0d3757 Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 18 Apr 2024 08:56:27 -0700 Subject: [PATCH 10/35] Add integration tests for multisig with distributed key generation (DKG) Also add a few more tests for trusted dealer key generation (TDK) --- .../wallet/multisig/integration.test.slow.ts | 221 ++++++++++++++++-- 1 file changed, 204 insertions(+), 17 deletions(-) diff --git a/ironfish/src/rpc/routes/wallet/multisig/integration.test.slow.ts b/ironfish/src/rpc/routes/wallet/multisig/integration.test.slow.ts index 7b5419e015..bad0c84265 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/integration.test.slow.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/integration.test.slow.ts @@ -6,27 +6,133 @@ import { Assert } from '../../../../assert' import { createRouteTest } from '../../../../testUtilities/routeTest' import { Account, ACCOUNT_SCHEMA_VERSION, AssertMultisigSigner } from '../../../../wallet' +function shuffleArray(array: Array): Array { + // Durstenfeld shuffle (https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm) + const shuffledArray = [...array] + for (let i = shuffledArray.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)) + ;[shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]] + } + return shuffledArray +} + describe('multisig RPC integration', () => { const routeTest = createRouteTest() - it('should create a verified transaction using multisig', async () => { - // create a bunch of multisig identities - const accountNames = Array.from({ length: 3 }, (_, index) => `test-account-${index}`) - const participants = await Promise.all( - accountNames.map(async (name) => { + describe('with TDK', () => { + // eslint-disable-next-line jest/expect-expect + it('should create a verified transaction using 2 signers (minumum: 2, maximum: 3)', async () => { + return runTest({ + setupMethod: setupWithTrustedDealer, + numSigners: 2, + minSigners: 2, + numParticipants: 3, + }) + }, 100000) + + // eslint-disable-next-line jest/expect-expect + it('should create a verified transaction using 5 signers (minumum: 3, maximum: 8)', async () => { + return runTest({ + setupMethod: setupWithTrustedDealer, + numSigners: 5, + minSigners: 3, + numParticipants: 8, + }) + }, 100000) + + // eslint-disable-next-line jest/expect-expect + it('should create a verified transaction using 3 signers (minumum: 3, maximum: 3)', () => { + return runTest({ + setupMethod: setupWithTrustedDealer, + numSigners: 3, + minSigners: 3, + numParticipants: 3, + }) + }, 100000) + }) + + describe('with DKG', () => { + // eslint-disable-next-line jest/expect-expect + it('should create a verified transaction using 2 signers (minumum: 2, maximum: 3)', async () => { + return runTest({ + setupMethod: setupWithDistributedKeyGen, + numSigners: 2, + minSigners: 2, + numParticipants: 3, + }) + }, 100000) + + // eslint-disable-next-line jest/expect-expect + it('should create a verified transaction using 5 signers (minumum: 3, maximum: 8)', async () => { + return runTest({ + setupMethod: setupWithDistributedKeyGen, + numSigners: 5, + minSigners: 3, + numParticipants: 8, + }) + }, 100000) + + // eslint-disable-next-line jest/expect-expect + it('should create a verified transaction using 3 signers (minumum: 3, maximum: 3)', () => { + return runTest({ + setupMethod: setupWithDistributedKeyGen, + numSigners: 3, + minSigners: 3, + numParticipants: 3, + }) + }, 100000) + }) + + async function runTest(options: { + numParticipants: number + minSigners: number + numSigners: number + setupMethod: (options: { + participants: Array<{ name: string; identity: string }> + minSigners: number + }) => Promise<{ participantAccounts: Array; coordinatorAccount: Account }> + }): Promise { + const { numParticipants, minSigners, numSigners, setupMethod } = options + const accountNames = Array.from( + { length: numParticipants }, + (_, index) => `test-account-${index}`, + ) + const participants = await createParticipants(accountNames) + const { participantAccounts, coordinatorAccount } = await setupMethod({ + participants, + minSigners, + }) + return createTransaction({ participantAccounts, coordinatorAccount, numSigners }) + } + + function createParticipants( + secretNames: Array, + ): Promise> { + return Promise.all( + secretNames.map(async (name) => { const identity = (await routeTest.client.wallet.multisig.createParticipant({ name })) .content.identity return { name, identity } }), ) + } - // initialize the group though tdk and import the accounts generated + async function setupWithTrustedDealer(options: { + participants: Array<{ name: string; identity: string }> + minSigners: number + }): Promise<{ participantAccounts: Array; coordinatorAccount: Account }> { + const { participants, minSigners } = options + + // create the trusted dealer packages const trustedDealerPackage = ( await routeTest.client.wallet.multisig.createTrustedDealerKeyPackage({ - minSigners: 2, + minSigners, participants, }) ).content + + // import the accounts generated by the trusted dealer + const participantAccounts = [] for (const { name, identity } of participants) { const importAccount = trustedDealerPackage.participantAccounts.find( (account) => account.identity === identity, @@ -35,15 +141,13 @@ describe('multisig RPC integration', () => { await routeTest.client.wallet.importAccount({ name, account: importAccount.account, + rescan: false, }) - } - // select only the first 2 accounts to sign (2 of 3) - const participantAccounts = accountNames.slice(0, 2).map((accountName) => { - const participantAccount = routeTest.wallet.getAccountByName(accountName) + const participantAccount = routeTest.wallet.getAccountByName(name) Assert.isNotNull(participantAccount) - return participantAccount - }) + participantAccounts.push(participantAccount) + } // import an account to serve as the coordinator await routeTest.client.wallet.importAccount({ @@ -59,16 +163,99 @@ describe('multisig RPC integration', () => { }, rescan: false, }) + + const coordinatorAccount = routeTest.wallet.getAccountByName('coordinator') + Assert.isNotNull(coordinatorAccount) + + return { participantAccounts, coordinatorAccount } + } + + async function setupWithDistributedKeyGen(options: { + participants: Array<{ name: string; identity: string }> + minSigners: number + }): Promise<{ participantAccounts: Array; coordinatorAccount: Account }> { + const { participants, minSigners } = options + + // perform dkg round 1 + const round1Packages = await Promise.all( + participants.map(({ name }) => + routeTest.client.wallet.multisig.dkg.round1({ + secretName: name, + minSigners, + participants, + }), + ), + ) + + // perform dkg round 2 + const round2Packages = await Promise.all( + participants.map(({ name }, index) => + routeTest.client.wallet.multisig.dkg.round2({ + secretName: name, + encryptedSecretPackage: round1Packages[index].content.encryptedSecretPackage, + publicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), + }), + ), + ) + + // perform dkg round 3 + const participantAccounts = await Promise.all( + participants.map(async ({ name }, index) => { + await routeTest.client.wallet.multisig.dkg.round3({ + secretName: name, + round2SecretPackage: round2Packages[index].content.encryptedSecretPackage, + round1PublicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), + round2PublicPackages: round2Packages.flatMap((pkg) => + pkg.content.publicPackages + .filter( + ({ recipientIdentity }) => recipientIdentity === participants[index].identity, + ) + .map(({ publicPackage }) => publicPackage), + ), + }) + + const participantAccount = routeTest.wallet.getAccountByName(name) + Assert.isNotNull(participantAccount) + return participantAccount + }), + ) + + const viewOnlyAccount = ( + await routeTest.client.wallet.exportAccount({ + account: participants[0].name, + viewOnly: true, + }) + ).content.account + Assert.isNotNull(viewOnlyAccount) + await routeTest.client.wallet.importAccount({ + name: 'coordinator', + account: viewOnlyAccount, + rescan: false, + }) + const coordinatorAccount = routeTest.wallet.getAccountByName('coordinator') Assert.isNotNull(coordinatorAccount) + return { participantAccounts, coordinatorAccount } + } + + async function createTransaction(options: { + participantAccounts: Array + coordinatorAccount: Account + numSigners: number + }) { + const { participantAccounts, coordinatorAccount, numSigners } = options + + // select `numSigners` random accounts to sign + const signerAccounts = shuffleArray(participantAccounts).slice(0, numSigners) + // fund coordinator account // mine block to send IRON to multisig account const miner = await routeTest.wallet.createAccount('miner') await fundAccount(coordinatorAccount, miner) // build list of signers - const signers = participantAccounts.map((participant) => { + const signers = signerAccounts.map((participant) => { AssertMultisigSigner(participant) const secret = new multisig.ParticipantSecret( Buffer.from(participant.multisigKeys.secret, 'hex'), @@ -98,7 +285,7 @@ describe('multisig RPC integration', () => { // create and collect signing commitments const commitments: Array = [] - for (const participantAccount of participantAccounts) { + for (const participantAccount of signerAccounts) { AssertMultisigSigner(participantAccount) const commitmentResponse = await routeTest.client.wallet.multisig.createSigningCommitment( @@ -121,7 +308,7 @@ describe('multisig RPC integration', () => { // create and collect signing shares const signatureShares: Array = [] - for (const participantAccount of participantAccounts) { + for (const participantAccount of signerAccounts) { AssertMultisigSigner(participantAccount) const signatureShareResponse = @@ -145,7 +332,7 @@ describe('multisig RPC integration', () => { Buffer.from(aggregateResponse.content.transaction, 'hex'), ]) expect(verified).toBe(true) - }, 100000) + } async function fundAccount(account: Account, miner: Account): Promise { Assert.isNotNull(miner.spendingKey) From 695087c14bb7e1d6710bdba95173f1b0c7703af6 Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Thu, 18 Apr 2024 16:30:09 -0400 Subject: [PATCH 11/35] Remove unused trusted setup code (#4896) --- Cargo.lock | 1 - .../src/commands/ceremony/contributions.ts | 65 --- ironfish-cli/src/commands/ceremony/index.ts | 246 ---------- ironfish-cli/src/commands/ceremony/service.ts | 108 ---- ironfish-cli/src/trusted-setup/client.ts | 142 ------ ironfish-cli/src/trusted-setup/schema.ts | 45 -- ironfish-cli/src/trusted-setup/server.ts | 460 ------------------ ironfish-cli/src/utils/s3.ts | 124 ----- ironfish-rust-nodejs/Cargo.toml | 1 - ironfish-rust-nodejs/src/lib.rs | 1 - ironfish-rust-nodejs/src/mpc.rs | 75 --- 11 files changed, 1268 deletions(-) delete mode 100644 ironfish-cli/src/commands/ceremony/contributions.ts delete mode 100644 ironfish-cli/src/commands/ceremony/index.ts delete mode 100644 ironfish-cli/src/commands/ceremony/service.ts delete mode 100644 ironfish-cli/src/trusted-setup/client.ts delete mode 100644 ironfish-cli/src/trusted-setup/schema.ts delete mode 100644 ironfish-cli/src/trusted-setup/server.ts delete mode 100644 ironfish-rust-nodejs/src/mpc.rs diff --git a/Cargo.lock b/Cargo.lock index 4c83ce7816..2647fd64c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1580,7 +1580,6 @@ dependencies = [ "fish_hash", "ironfish", "ironfish-frost", - "ironfish_mpc", "jubjub 0.9.0 (git+https://github.com/iron-fish/jubjub.git?branch=blstrs)", "napi", "napi-build", diff --git a/ironfish-cli/src/commands/ceremony/contributions.ts b/ironfish-cli/src/commands/ceremony/contributions.ts deleted file mode 100644 index 0603987f5c..0000000000 --- a/ironfish-cli/src/commands/ceremony/contributions.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { S3Client } from '@aws-sdk/client-s3' -import { Flags } from '@oclif/core' -import { IronfishCommand } from '../../command' -import { S3Utils } from '../../utils' - -export default class CeremonyContributions extends IronfishCommand { - static description = 'List all the current contributions with names' - - static flags = { - start: Flags.integer({ - required: false, - }), - end: Flags.integer({ - required: false, - }), - } - - async start(): Promise { - const { flags } = await this.parse(CeremonyContributions) - - const r2Credentials = await S3Utils.getR2Credentials() - - if (r2Credentials === undefined) { - this.logger.log('Failed getting R2 credentials from AWS') - this.exit(0) - return - } - - const r2Client = S3Utils.getR2S3Client(r2Credentials) - - const latestParamName = await this.getLatestParamName(r2Client, 'ironfish-contributions') - const latestParamNumber = parseInt(latestParamName.split('_')[1]) - const keys: string[] = [...new Array(latestParamNumber + 1)] - .map((_, i) => i) - .filter((i) => (!flags.start || i >= flags.start) && (!flags.end || i <= flags.end)) - .map((i) => { - return 'params_' + i.toString().padStart(5, '0') - }) - - for (const key of keys) { - const { Metadata } = await S3Utils.getObjectMetadata( - r2Client, - 'ironfish-contributions', - key, - ) - this.log( - `Contribution: ${key.split('_')[1]}, Name: ${Metadata?.contributorName || '-'}, IP: ${ - Metadata?.remoteaddress || '-' - }`, - ) - } - } - - async getLatestParamName(client: S3Client, bucket: string): Promise { - const paramFileNames = await S3Utils.getBucketObjects(client, bucket) - const validParams = paramFileNames - .slice(0) - .filter((fileName) => /^params_\d{5}$/.test(fileName)) - validParams.sort() - return validParams[validParams.length - 1] - } -} diff --git a/ironfish-cli/src/commands/ceremony/index.ts b/ironfish-cli/src/commands/ceremony/index.ts deleted file mode 100644 index 3f9451e952..0000000000 --- a/ironfish-cli/src/commands/ceremony/index.ts +++ /dev/null @@ -1,246 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { contribute } from '@ironfish/rust-nodejs' -import { ErrorUtils, PromiseUtils, TimeUtils } from '@ironfish/sdk' -import { CliUx, Flags } from '@oclif/core' -import axios from 'axios' -import fsAsync from 'fs/promises' -import path from 'path' -import { pipeline } from 'stream/promises' -import { IronfishCommand } from '../../command' -import { DataDirFlag, DataDirFlagKey, VerboseFlag, VerboseFlagKey } from '../../flags' -import { CeremonyClient } from '../../trusted-setup/client' - -export default class Ceremony extends IronfishCommand { - static description = 'Contribute randomness to the Iron Fish trusted setup' - - static flags = { - [VerboseFlagKey]: VerboseFlag, - [DataDirFlagKey]: DataDirFlag, - host: Flags.string({ - parse: (input: string) => Promise.resolve(input.trim()), - default: 'ceremony.ironfish.network', - description: 'Host address of the ceremony coordination server', - }), - port: Flags.integer({ - default: 9040, - description: 'Port of the ceremony coordination server', - }), - token: Flags.string({ - required: false, - }), - } - - async start(): Promise { - const { flags } = await this.parse(Ceremony) - const { host, port } = flags - - // Pre-make the temp directory to check for access - const tempDir = this.sdk.config.tempDir - await fsAsync.mkdir(tempDir, { recursive: true }) - - const inputPath = path.join(tempDir, 'params') - const outputPath = path.join(tempDir, 'newParams') - - let localHash: string | null = null - let refreshEtaInterval: NodeJS.Timeout | null = null - let etaDate: Date | null = null - - // Prompt for randomness - let randomness: string | null = await CliUx.ux.prompt( - `If you'd like to contribute your own randomness to the ceremony, type it here, then press Enter. For more information on where this should come from and its importance, please read https://setup.ironfish.network. If you'd like the command to generate some randomness for you, just press Enter`, - { required: false }, - ) - randomness = randomness.length ? randomness : null - - const name = await CliUx.ux.prompt( - `If you'd like to associate a name with this contribution, type it here, then press Enter. Otherwise, to contribute anonymously, just press Enter`, - { required: false }, - ) - - // Create the client and bind events - const client = new CeremonyClient({ - host, - port, - logger: this.logger.withTag('ceremonyClient'), - }) - - client.onJoined.on(({ queueLocation, estimate }) => { - refreshEtaInterval && clearInterval(refreshEtaInterval) - - etaDate = new Date(Date.now() + estimate) - - CliUx.ux.action.status = renderStatus(queueLocation, etaDate) - refreshEtaInterval = setInterval(() => { - CliUx.ux.action.status = renderStatus(queueLocation, etaDate) - }, 10 * 1000) - - CliUx.ux.action.status = `Current position: ${queueLocation}` - }) - - client.onInitiateContribution.on(async ({ downloadLink, contributionNumber }) => { - CliUx.ux.action.stop() - refreshEtaInterval && clearInterval(refreshEtaInterval) - - this.log(`Starting contribution. You are contributor #${contributionNumber}`) - - CliUx.ux.action.start(`Downloading the previous contribution to ${inputPath}`) - - const fileHandle = await fsAsync.open(inputPath, 'w') - - let response - try { - response = await axios.get(downloadLink, { - responseType: 'stream', - onDownloadProgress: (p: { - readonly lengthComputable: boolean - readonly loaded: number - readonly total: number - }) => { - this.log('loaded', p.loaded, 'total', p.total) - }, - }) - } catch (e) { - this.error(ErrorUtils.renderError(e)) - } - - await pipeline(response.data, fileHandle.createWriteStream()) - - CliUx.ux.action.stop() - - CliUx.ux.action.start(`Contributing your randomness`) - - localHash = await contribute(inputPath, outputPath, randomness) - - CliUx.ux.action.stop() - - CliUx.ux.action.start(`Waiting to upload your contribution`) - - client.contributionComplete() - }) - - client.onInitiateUpload.on(async ({ uploadLink }) => { - CliUx.ux.action.stop() - refreshEtaInterval && clearInterval(refreshEtaInterval) - - CliUx.ux.action.start(`Uploading your contribution`) - - const fileHandle = await fsAsync.open(outputPath, 'r') - const stat = await fsAsync.stat(outputPath) - - try { - await axios.put(uploadLink, fileHandle.createReadStream(), { - // Axios requires specifying some max body length when uploading. - // Set it high enough that we're not likely to hit it - maxBodyLength: 1000000000, - headers: { - 'Content-Type': 'application/octet-stream', - 'Content-Length': stat.size, - }, - }) - } catch (e) { - this.error(ErrorUtils.renderError(e)) - } - - CliUx.ux.action.stop() - client.uploadComplete() - - CliUx.ux.action.start('Contribution uploaded. Waiting for server to verify') - }) - - client.onContributionVerified.on(({ hash, downloadLink, contributionNumber }) => { - CliUx.ux.action.stop() - refreshEtaInterval && clearInterval(refreshEtaInterval) - - if (!localHash) { - this.log( - `Server verified a contribution, but you haven't made a contribution yet. Server-generated hash:`, - ) - this.log(display256CharacterHash(hash)) - this.error('Please contact the Iron Fish team with this error message.') - } - - if (hash !== localHash) { - this.log('Hashes do not match. Locally generated hash:') - this.log(display256CharacterHash(localHash)) - this.log('Server-generated hash:') - this.log(display256CharacterHash(hash)) - this.error('Please contact the Iron Fish team with this error message.') - } - - this.log( - `\nThank you for your contribution to the Iron Fish Ceremony. You have successfully contributed at position #${contributionNumber}. The public hash of your contribution is:`, - ) - this.log(display256CharacterHash(hash)) - this.log( - `This hash is a record of your contribution to the Iron Fish parameters, so you should save it to check later. You can view your contributed file at ${downloadLink}.`, - ) - - client.stop(true) - this.exit(0) - }) - - client.onStopRetry.on(({ error }) => { - CliUx.ux.action.stop() - refreshEtaInterval && clearInterval(refreshEtaInterval) - - this.log(`Stopping contribution: ${error}`) - - client.stop(true) - }) - - // Retry connection until contributions are received - let connected = false - while (!connected) { - CliUx.ux.action.start('Connecting') - const error = await client.start() - connected = error === null - CliUx.ux.action.stop(error ? `Error connecting: ${error}` : 'done') - - if (!connected) { - this.log('Unable to connect to contribution server. Retrying in 5 seconds.') - await PromiseUtils.sleep(5000) - continue - } - - client.join(name, flags.token) - - CliUx.ux.action.start('Waiting to contribute', undefined, { stdout: true }) - - const result = await client.waitForStop() - connected = result.stopRetries - - if (!connected) { - if (CliUx.ux.action.running) { - CliUx.ux.action.stop('error') - } - this.log( - `We're sorry, but your contribution either timed out or you lost connection. Attempting to connect again in 5 seconds.`, - ) - await PromiseUtils.sleep(5000) - } - } - } -} - -const renderStatus = (queueLocation: number, etaDate: Date | null): string => { - return `Current position: ${queueLocation} ${ - etaDate - ? `(Estimated time remaining: ${TimeUtils.renderSpan(etaDate.getTime() - Date.now())})` - : '' - }` -} - -const display256CharacterHash = (hash: string): string => { - // split string every 8 characters - let slices: string[] = hash.match(/.{1,8}/g) ?? [] - - const output = [] - for (let i = 0; i < 4; i++) { - output.push(`\t${slices.slice(0, 4).join(' ')}`) - slices = slices.slice(4) - } - - return `\n${output.join('\n')}\n` -} diff --git a/ironfish-cli/src/commands/ceremony/service.ts b/ironfish-cli/src/commands/ceremony/service.ts deleted file mode 100644 index 374f3d2a69..0000000000 --- a/ironfish-cli/src/commands/ceremony/service.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { createRootLogger, setLogPrefixFromConfig } from '@ironfish/sdk' -import { Flags } from '@oclif/core' -import { IronfishCommand } from '../../command' -import { RemoteFlags } from '../../flags' -import { CeremonyServer } from '../../trusted-setup/server' -import { S3Utils } from '../../utils' - -const CONTRIBUTE_TIMEOUT_MS = 5 * 60 * 1000 -const UPLOAD_TIMEOUT_MS = 5 * 60 * 1000 -const PRESIGNED_EXPIRATION_SEC = 5 * 60 -const START_DATE = 1680904800000 // Friday, April 07 2023 15:00:00 GMT-0700 (Pacific Daylight Time) - -export default class CeremonyService extends IronfishCommand { - static hidden = true - - static description = ` - Start the coordination server for the Iron Fish trusted setup ceremony - ` - - static flags = { - ...RemoteFlags, - bucket: Flags.string({ - char: 'b', - parse: (input: string) => Promise.resolve(input.trim()), - required: false, - description: 'S3/R2 bucket to download and upload params to', - default: 'ironfish-contributions', - }), - downloadPrefix: Flags.string({ - char: 'b', - parse: (input: string) => Promise.resolve(input.trim()), - required: false, - description: 'Prefix for contribution download URLs', - default: 'https://contributions.ironfish.network', - }), - contributionTimeoutMs: Flags.integer({ - required: false, - description: 'Allowable milliseconds for a contributor to run the contribution script', - default: CONTRIBUTE_TIMEOUT_MS, - }), - uploadTimeoutMs: Flags.integer({ - required: false, - description: 'Allowable milliseconds for a contributor to upload their new parameters', - default: UPLOAD_TIMEOUT_MS, - }), - presignedExpirationSec: Flags.integer({ - required: false, - description: - 'How many seconds the S3/R2 pre-signed upload URL is valid for a contributor', - default: PRESIGNED_EXPIRATION_SEC, - }), - startDate: Flags.integer({ - required: false, - description: 'When should the server start accepting contributions', - default: START_DATE, - }), - token: Flags.string({ - required: true, - }), - skipIPCheck: Flags.boolean({ - required: false, - description: 'Pass this flag if you want to skip checking for duplicate IPs', - default: false, - }), - } - - async start(): Promise { - const { flags } = await this.parse(CeremonyService) - - const DEFAULT_HOST = '0.0.0.0' - const DEFAULT_PORT = 9040 - - const r2Credentials = await S3Utils.getR2Credentials('us-east-1') - - if (r2Credentials === undefined) { - this.logger.log('Failed getting R2 credentials from AWS') - this.exit(0) - return - } - - const r2Client = S3Utils.getR2S3Client(r2Credentials) - - setLogPrefixFromConfig(`[%tag%]`) - - const server = new CeremonyServer({ - logger: createRootLogger(), - port: DEFAULT_PORT, - host: DEFAULT_HOST, - s3Bucket: flags.bucket, - downloadPrefix: flags.downloadPrefix, - s3Client: r2Client, - tempDir: this.sdk.config.tempDir, - contributionTimeoutMs: flags.contributionTimeoutMs, - uploadTimeoutMs: flags.uploadTimeoutMs, - presignedExpirationSec: flags.presignedExpirationSec, - startDate: flags.startDate, - token: flags.token, - enableIPBanning: !flags.skipIPCheck, - }) - - await server.start() - - await server.waitForStop() - } -} diff --git a/ironfish-cli/src/trusted-setup/client.ts b/ironfish-cli/src/trusted-setup/client.ts deleted file mode 100644 index 0fba3b4168..0000000000 --- a/ironfish-cli/src/trusted-setup/client.ts +++ /dev/null @@ -1,142 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Assert, ErrorUtils, Event, Logger, MessageBuffer } from '@ironfish/sdk' -import net from 'net' -import { CeremonyClientMessage, CeremonyServerMessage } from './schema' - -export class CeremonyClient { - readonly socket: net.Socket - readonly host: string - readonly port: number - readonly logger: Logger - readonly messageBuffer: MessageBuffer - - private stopPromise: Promise<{ stopRetries: boolean }> | null = null - private stopResolve: ((params: { stopRetries: boolean }) => void) | null = null - - readonly onJoined = new Event<[{ queueLocation: number; estimate: number }]>() - readonly onInitiateUpload = new Event<[{ uploadLink: string }]>() - readonly onInitiateContribution = new Event< - [{ downloadLink: string; contributionNumber: number }] - >() - readonly onContributionVerified = new Event< - [{ hash: string; downloadLink: string; contributionNumber: number }] - >() - readonly onStopRetry = new Event<[{ error: string }]>() - - constructor(options: { host: string; port: number; logger: Logger }) { - this.host = options.host - this.port = options.port - this.logger = options.logger - this.messageBuffer = new MessageBuffer('\n') - - this.socket = new net.Socket() - this.socket.on('data', (data) => void this.onData(data)) - } - - async start(): Promise { - this.stopPromise = new Promise((r) => (this.stopResolve = r)) - - const error = await connectSocket(this.socket, this.host, this.port) - .then(() => null) - .catch((e) => ErrorUtils.renderError(e)) - - if (error === null) { - this.socket.on('error', this.onError) - this.socket.on('close', this.onDisconnect) - } - - return error - } - - stop(stopRetries: boolean): void { - this.socket.end() - this.stopResolve && this.stopResolve({ stopRetries }) - this.stopPromise = null - this.stopResolve = null - } - - waitForStop(): Promise<{ stopRetries: boolean }> { - Assert.isNotNull(this.stopPromise, 'Cannot wait for stop before starting') - return this.stopPromise - } - - contributionComplete(): void { - this.send({ method: 'contribution-complete' }) - } - - join(name: string, token?: string): void { - this.send({ method: 'join', name, token }) - } - - uploadComplete(): void { - this.send({ method: 'upload-complete' }) - } - - private send(message: CeremonyClientMessage): void { - this.socket.write(JSON.stringify(message) + '\n') - } - - private onDisconnect = (): void => { - this.stop(false) - this.socket.off('error', this.onError) - this.socket.off('close', this.onDisconnect) - } - - private onError = (error: unknown): void => { - this.logger.error(`Server error ${ErrorUtils.renderError(error)}`) - } - - private onData(data: Buffer): void { - this.messageBuffer.write(data) - - for (const message of this.messageBuffer.readMessages()) { - let parsedMessage - try { - parsedMessage = JSON.parse(message) as CeremonyServerMessage - } catch { - this.logger.debug(`Received unknown message: ${message}`) - return - } - - if (parsedMessage.method === 'joined') { - this.onJoined.emit({ - queueLocation: parsedMessage.queueLocation, - estimate: parsedMessage.estimate, - }) - } else if (parsedMessage.method === 'initiate-upload') { - this.onInitiateUpload.emit({ uploadLink: parsedMessage.uploadLink }) - } else if (parsedMessage.method === 'initiate-contribution') { - this.onInitiateContribution.emit(parsedMessage) - } else if (parsedMessage.method === 'contribution-verified') { - this.onContributionVerified.emit(parsedMessage) - } else if (parsedMessage.method === 'disconnect') { - this.onStopRetry.emit({ error: parsedMessage.error }) - } else { - this.logger.info(`Received message: ${message}`) - } - } - } -} - -// Transform net.Socket.connect() callback into a nicer promise style interface -function connectSocket(socket: net.Socket, host: string, port: number): Promise { - return new Promise((resolve, reject): void => { - const onConnect = () => { - socket.off('connect', onConnect) - socket.off('error', onError) - resolve() - } - - const onError = (error: unknown) => { - socket.off('connect', onConnect) - socket.off('error', onError) - reject(error) - } - - socket.on('error', onError) - socket.on('connect', onConnect) - socket.connect(port, host) - }) -} diff --git a/ironfish-cli/src/trusted-setup/schema.ts b/ironfish-cli/src/trusted-setup/schema.ts deleted file mode 100644 index 0851d9f671..0000000000 --- a/ironfish-cli/src/trusted-setup/schema.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -import * as yup from 'yup' - -export type CeremonyServerMessage = - | { - method: 'joined' - queueLocation: number - estimate: number - } - | { - method: 'initiate-contribution' - downloadLink: string - contributionNumber: number - } - | { - method: 'initiate-upload' - uploadLink: string - } - | { - method: 'contribution-verified' - hash: string - downloadLink: string - contributionNumber: number - } - | { - method: 'disconnect' - error: string - } - -export type CeremonyClientMessage = { - method: 'contribution-complete' | 'upload-complete' | 'join' - name?: string // only used on join - token?: string // only used on join -} - -export const CeremonyClientMessageSchema: yup.ObjectSchema = yup - .object({ - method: yup.string().oneOf(['contribution-complete', 'upload-complete', 'join']).required(), - name: yup.string(), - token: yup.string(), - }) - .required() diff --git a/ironfish-cli/src/trusted-setup/server.ts b/ironfish-cli/src/trusted-setup/server.ts deleted file mode 100644 index df0f5a360d..0000000000 --- a/ironfish-cli/src/trusted-setup/server.ts +++ /dev/null @@ -1,460 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { S3Client } from '@aws-sdk/client-s3' -import { verifyTransform } from '@ironfish/rust-nodejs' -import { ErrorUtils, Logger, MessageBuffer, SetTimeoutToken, YupUtils } from '@ironfish/sdk' -import fsAsync from 'fs/promises' -import net from 'net' -import path from 'path' -import { v4 as uuid } from 'uuid' -import { S3Utils } from '../utils' -import { CeremonyClientMessageSchema, CeremonyServerMessage } from './schema' - -type CurrentContributor = - | { - state: 'STARTED' - client: CeremonyServerClient - actionTimeout: SetTimeoutToken - } - | { - state: 'UPLOADING' - client: CeremonyServerClient - actionTimeout: SetTimeoutToken - } - | { - state: 'VERIFYING' - client: CeremonyServerClient | null - } - -class CeremonyServerClient { - id: string - socket: net.Socket - connected: boolean - logger: Logger - readonly messageBuffer: MessageBuffer - - private _joined?: { - name?: string - } - - constructor(options: { socket: net.Socket; id: string; logger: Logger }) { - this.messageBuffer = new MessageBuffer('\n') - this.id = options.id - this.socket = options.socket - this.connected = true - this.logger = options.logger - .withTag(`client:${this.id.slice(0, 4)}..${this.id.slice(-4)}`) - .withTag(`ip:${this.socket.remoteAddress || 'unknown'}`) - } - - send(message: CeremonyServerMessage): void { - this.socket.write(JSON.stringify(message) + '\n') - } - - join(name?: string) { - this._joined = { name } - } - - get joined(): boolean { - return this._joined !== undefined - } - - get name(): string | undefined { - return this._joined?.name - } - - close(error?: Error): void { - if (!this.connected) { - return - } - - this.connected = false - this.socket.destroy(error) - } -} - -export class CeremonyServer { - readonly server: net.Server - readonly logger: Logger - - private stopPromise: Promise | null = null - private stopResolve: (() => void) | null = null - - readonly port: number - readonly host: string - - readonly s3Bucket: string - readonly downloadPrefix: string - private s3Client: S3Client - - readonly tempDir: string - - private queue: CeremonyServerClient[] = [] - private privateQueue: CeremonyServerClient[] = [] - - private currentContributor: CurrentContributor | null = null - - readonly contributionTimeoutMs: number - readonly uploadTimeoutMs: number - readonly presignedExpirationSec: number - - readonly startDate: number - private token: string - - readonly enableIPBanning: boolean - - constructor(options: { - logger: Logger - port: number - host: string - s3Bucket: string - downloadPrefix: string - s3Client: S3Client - tempDir: string - contributionTimeoutMs: number - uploadTimeoutMs: number - presignedExpirationSec: number - startDate: number - token: string - enableIPBanning: boolean - }) { - this.logger = options.logger - - this.host = options.host - this.port = options.port - - this.tempDir = options.tempDir - - this.s3Bucket = options.s3Bucket - this.downloadPrefix = options.downloadPrefix - this.s3Client = options.s3Client - - this.contributionTimeoutMs = options.contributionTimeoutMs - this.uploadTimeoutMs = options.uploadTimeoutMs - this.presignedExpirationSec = options.presignedExpirationSec - - this.startDate = options.startDate - this.token = options.token - - this.enableIPBanning = options.enableIPBanning - - this.server = net.createServer((s) => this.onConnection(s)) - } - - async getLatestParamName(): Promise { - const paramFileNames = await S3Utils.getBucketObjects(this.s3Client, this.s3Bucket) - const validParams = paramFileNames - .slice(0) - .filter((fileName) => /^params_\d{5}$/.test(fileName)) - validParams.sort() - return validParams[validParams.length - 1] - } - - totalQueueLength(): number { - return this.queue.length + this.privateQueue.length - } - - closeClient(client: CeremonyServerClient, error?: Error, disconnect = false): void { - if (this.currentContributor?.client?.id === client.id) { - if (this.currentContributor.state === 'VERIFYING') { - this.currentContributor.client = null - } else { - clearTimeout(this.currentContributor.actionTimeout) - this.currentContributor = null - void this.startNextContributor() - } - } - - disconnect && client.send({ method: 'disconnect', error: ErrorUtils.renderError(error) }) - - client.close(error) - } - - /** initiate a contributor if one does not already exist */ - async startNextContributor(): Promise { - if (this.currentContributor !== null) { - return - } - - const nextClient = this.privateQueue.shift() || this.queue.shift() - if (!nextClient) { - return - } - - const contributionTimeout = setTimeout(() => { - this.closeClient(nextClient, new Error('Failed to complete contribution in time')) - }, this.contributionTimeoutMs) - - this.currentContributor = { - state: 'STARTED', - client: nextClient, - actionTimeout: contributionTimeout, - } - - const latestParamName = await this.getLatestParamName() - const nextParamNumber = parseInt(latestParamName.split('_')[1]) + 1 - - nextClient.logger.info(`Starting contribution ${nextParamNumber}`) - - nextClient.send({ - method: 'initiate-contribution', - downloadLink: `${this.downloadPrefix}/${latestParamName}`, - contributionNumber: nextParamNumber, - }) - } - - async start(): Promise { - // Pre-make the directories to check for access - await fsAsync.mkdir(this.tempDir, { recursive: true }) - - this.stopPromise = new Promise((r) => (this.stopResolve = r)) - this.server.listen(this.port, this.host) - this.logger.info(`Server started at ${this.host}:${this.port}`) - } - - stop(): void { - this.server.close() - this.stopResolve && this.stopResolve() - this.stopPromise = null - this.stopResolve = null - this.logger.info(`Server stopped on ${this.host}:${this.port}`) - } - - async waitForStop(): Promise { - await this.stopPromise - } - - private onConnection(socket: net.Socket): void { - const client = new CeremonyServerClient({ socket, id: uuid(), logger: this.logger }) - - socket.on('data', (data: Buffer) => void this.onData(client, data)) - socket.on('close', () => this.onDisconnect(client)) - socket.on('error', (e) => this.onDisconnect(client, e)) - - const ip = socket.remoteAddress - if ( - this.enableIPBanning && - (ip === undefined || - this.queue.find((c) => c.socket.remoteAddress === ip) !== undefined || - this.privateQueue.find((c) => c.socket.remoteAddress === ip) !== undefined || - this.currentContributor?.client?.socket.remoteAddress === ip) - ) { - this.closeClient(client, new Error('IP address already in queue'), true) - return - } - } - - private onDisconnect(client: CeremonyServerClient, e?: Error): void { - this.closeClient(client, e) - this.queue = this.queue.filter((c) => client.id !== c.id) - this.privateQueue = this.privateQueue.filter((c) => client.id !== c.id) - - e && client.logger.info(`Disconnected with error: ${ErrorUtils.renderError(e)}`) - client.logger.info( - `(Disconnected) public: ${this.queue.length}, private: ${this.privateQueue.length}`, - ) - } - - private async onData(client: CeremonyServerClient, data: Buffer): Promise { - client.messageBuffer.write(data) - - for (const message of client.messageBuffer.readMessages()) { - const result = await YupUtils.tryValidate(CeremonyClientMessageSchema, message) - if (result.error) { - client.logger.error(`Could not parse client message: ${message}`) - this.closeClient(client, new Error(`Could not parse message`), true) - return - } - - const parsedMessage = result.result - - client.logger.info(`Message Received: ${parsedMessage.method}`) - - if (parsedMessage.method === 'join' && !client.joined) { - if (Date.now() < this.startDate && parsedMessage.token !== this.token) { - this.closeClient( - client, - new Error( - `The ceremony does not start until ${new Date(this.startDate).toUTCString()}`, - ), - true, - ) - return - } - - client.join(parsedMessage.name) - - if (parsedMessage.token === this.token) { - this.privateQueue.push(client) - client.send(this.getJoinedMessage(this.privateQueue.length)) - } else { - this.queue.push(client) - client.send(this.getJoinedMessage(this.totalQueueLength())) - } - - client.logger.info( - `(Connected) public: ${this.queue.length}, private: ${this.privateQueue.length}`, - ) - void this.startNextContributor() - } else if (parsedMessage.method === 'contribution-complete') { - await this.handleContributionComplete(client).catch((e) => { - client.logger.error( - `Error handling contribution-complete: ${ErrorUtils.renderError(e)}`, - ) - this.closeClient(client, new Error(`Error generating upload url`)) - }) - } else if (parsedMessage.method === 'upload-complete') { - await this.handleUploadComplete(client).catch((e) => { - client.logger.error(`Error handling upload-complete: ${ErrorUtils.renderError(e)}`) - - this.closeClient(client) - - if (this.currentContributor?.client == null) { - this.currentContributor = null - void this.startNextContributor() - } - }) - } else { - client.logger.error(`Unknown method received: ${message}`) - this.closeClient(client, new Error(`Unknown method received`)) - } - } - } - - private async handleContributionComplete(client: CeremonyServerClient) { - if ( - this.currentContributor?.client?.id !== client.id || - this.currentContributor.state !== 'STARTED' - ) { - throw new Error('contribution-complete message sent but not the current contributor') - } - - clearTimeout(this.currentContributor.actionTimeout) - - client.logger.info('Generating presigned URL') - - const presignedUrl = await S3Utils.getPresignedUploadUrl( - this.s3Client, - this.s3Bucket, - client.id, - this.presignedExpirationSec, - ) - - client.logger.info('Sending back presigned URL') - - client.send({ - method: 'initiate-upload', - uploadLink: presignedUrl, - }) - - this.currentContributor = { - state: 'UPLOADING', - actionTimeout: setTimeout(() => { - this.closeClient(client, new Error('Failed to complete upload in time')) - }, this.uploadTimeoutMs), - client: this.currentContributor.client, - } - } - - private getJoinedMessage(position: number): CeremonyServerMessage { - const estimate = position * ((this.contributionTimeoutMs + this.uploadTimeoutMs) / 2) - return { method: 'joined', queueLocation: position, estimate } - } - - private sendUpdatedLocationsToClients() { - for (const [i, client] of this.privateQueue.entries()) { - client.send(this.getJoinedMessage(i + 1)) - } - - for (const [i, client] of this.queue.entries()) { - client.send(this.getJoinedMessage(i + 1 + this.privateQueue.length)) - } - } - - private async handleUploadComplete(client: CeremonyServerClient) { - if ( - this.currentContributor?.client?.id !== client.id || - this.currentContributor.state !== 'UPLOADING' - ) { - throw new Error('upload-complete message sent but not the current contributor') - } - - clearTimeout(this.currentContributor.actionTimeout) - - this.currentContributor = { - state: 'VERIFYING', - client: this.currentContributor.client, - } - - client.logger.info('Getting latest contribution from S3') - const latestParamName = await this.getLatestParamName() - const nextParamNumber = parseInt(latestParamName.split('_')[1]) + 1 - - const oldParamsDownloadPath = path.join(this.tempDir, latestParamName) - - const paramsExist = await fsAsync - .access(oldParamsDownloadPath) - .then((_) => true) - .catch((_) => false) - - const oldParamsPromise = paramsExist - ? Promise.resolve() - : S3Utils.downloadFromBucket( - this.s3Client, - this.s3Bucket, - latestParamName, - oldParamsDownloadPath, - ) - - const newParamsDownloadPath = path.join(this.tempDir, client.id) - const newParamsPromise = S3Utils.downloadFromBucket( - this.s3Client, - this.s3Bucket, - client.id, - newParamsDownloadPath, - ) - - client.logger.info(`Downloading params from S3 to verify`) - await Promise.all([oldParamsPromise, newParamsPromise]) - - client.logger.info(`Deleting uploaded params from S3`) - await S3Utils.deleteFromBucket(this.s3Client, this.s3Bucket, client.id) - - client.logger.info(`Verifying contribution`) - const hash = await verifyTransform(oldParamsDownloadPath, newParamsDownloadPath) - - client.logger.info(`Uploading verified contribution`) - const destFile = 'params_' + nextParamNumber.toString().padStart(5, '0') - - const metadata = { - ...(client.name && { contributorName: encodeURIComponent(client.name) }), - } - - await S3Utils.uploadToBucket( - this.s3Client, - newParamsDownloadPath, - 'application/octet-stream', - this.s3Bucket, - destFile, - client.logger, - metadata, - ) - - client.logger.info(`Cleaning up local files`) - await fsAsync.rename(newParamsDownloadPath, path.join(this.tempDir, destFile)) - await fsAsync.rm(oldParamsDownloadPath) - - client.send({ - method: 'contribution-verified', - hash, - downloadLink: `${this.downloadPrefix}/${destFile}`, - contributionNumber: nextParamNumber, - }) - - client.logger.info(`Contribution ${nextParamNumber} complete`) - this.currentContributor = null - await this.startNextContributor() - this.sendUpdatedLocationsToClients() - } -} diff --git a/ironfish-cli/src/utils/s3.ts b/ironfish-cli/src/utils/s3.ts index af30e5e87a..d47f5d8fa9 100644 --- a/ironfish-cli/src/utils/s3.ts +++ b/ironfish-cli/src/utils/s3.ts @@ -2,29 +2,17 @@ * 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 { Readable } from 'stream' import { CognitoIdentity } from '@aws-sdk/client-cognito-identity' import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, - DeleteObjectCommand, - DeleteObjectCommandOutput, - GetObjectCommand, - HeadObjectCommand, - HeadObjectCommandOutput, - ListObjectsCommand, - ListObjectsCommandInput, - PutObjectCommand, S3Client, UploadPartCommand, } from '@aws-sdk/client-s3' -import { GetSecretValueCommand, SecretsManagerClient } from '@aws-sdk/client-secrets-manager' -import { getSignedUrl } from '@aws-sdk/s3-request-presigner' import { Credentials } from '@aws-sdk/types/dist-types/credentials' import { Assert, ErrorUtils, Logger } from '@ironfish/sdk' import fsAsync from 'fs/promises' -import { pipeline } from 'stream/promises' // AWS requires that upload parts be at least 5MB const MINIMUM_MULTIPART_FILE_SIZE = 5 * 1024 * 1024 @@ -45,9 +33,6 @@ class UploadLastMultipartError extends UploadToBucketError {} class UploadReadFileError extends UploadToBucketError {} class UploadFailedError extends UploadToBucketError {} -const R2_SECRET_NAME = 'r2-prod-access-key' -const R2_ENDPOINT = `https://a93bebf26da4c2fe205f71c896afcf89.r2.cloudflarestorage.com` - export type R2Secret = { r2AccessKeyId: string r2SecretAccessKey: string @@ -205,43 +190,6 @@ async function uploadToBucket( }) } -async function downloadFromBucket( - s3: S3Client, - bucket: string, - keyName: string, - output: string, -): Promise { - const command = new GetObjectCommand({ Bucket: bucket, Key: keyName }) - const response = await s3.send(command) - if (response.Body) { - const fileHandle = await fsAsync.open(output, 'w') - const ws = fileHandle.createWriteStream() - - await pipeline(response.Body as Readable, ws) - - ws.close() - await fileHandle.close() - } -} - -async function getPresignedUploadUrl( - s3: S3Client, - bucket: string, - keyName: string, - expiresInSeconds: number, -): Promise { - const command = new PutObjectCommand({ - Bucket: bucket, - Key: keyName, - }) - - const signedUrl = await getSignedUrl(s3, command, { - expiresIn: expiresInSeconds, - }) - - return signedUrl -} - /** * Returns an HTTPS URL to a file in S3. * https://docs.aws.amazon.com/AmazonS3/latest/userguide/transfer-acceleration-getting-started.html @@ -265,46 +213,6 @@ function getDownloadUrl( return `https://${bucket}.${regionString}.amazonaws.com/${key}` } -async function getObjectMetadata( - s3: S3Client, - bucket: string, - key: string, -): Promise { - const command = new HeadObjectCommand({ Bucket: bucket, Key: key }) - const response = await s3.send(command) - return response -} - -async function getBucketObjects(s3: S3Client, bucket: string): Promise { - let truncated = true - let commandParams: ListObjectsCommandInput = { Bucket: bucket } - const keys: string[] = [] - - while (truncated) { - const command = new ListObjectsCommand(commandParams) - const response = await s3.send(command) - - for (const obj of response.Contents || []) { - if (obj.Key !== undefined) { - keys.push(obj.Key) - } - } - - truncated = response.IsTruncated || false - commandParams = { Bucket: bucket, Marker: response.Contents?.slice(-1)[0]?.Key } - } - - return keys -} - -async function deleteFromBucket( - s3Client: S3Client, - bucket: string, - fileName: string, -): Promise { - return s3Client.send(new DeleteObjectCommand({ Bucket: bucket, Key: fileName })) -} - function getS3Client( useDualstackEndpoint: boolean, credentials?: { @@ -330,31 +238,6 @@ function getS3Client( }) } -function getR2S3Client(credentials: { - r2AccessKeyId: string - r2SecretAccessKey: string -}): S3Client { - return new S3Client({ - region: 'auto', - endpoint: R2_ENDPOINT, - credentials: { - accessKeyId: credentials.r2AccessKeyId, - secretAccessKey: credentials.r2SecretAccessKey, - }, - }) -} - -async function getR2Credentials(region?: string): Promise { - const client = new SecretsManagerClient({ region }) - const command = new GetSecretValueCommand({ SecretId: R2_SECRET_NAME }) - const response = await client.send(command) - if (response.SecretString === undefined) { - return - } else { - return JSON.parse(response.SecretString) as R2Secret - } -} - async function getCognitoIdentityCredentials(): Promise { const identityPoolId = 'us-east-1:3ebc542a-6ac4-4c5d-9558-1621eadd2382' @@ -384,15 +267,8 @@ async function getCognitoIdentityCredentials(): Promise { } export const S3Utils = { - deleteFromBucket, - downloadFromBucket, - getBucketObjects, getCognitoIdentityCredentials, getDownloadUrl, - getObjectMetadata, - getPresignedUploadUrl, - getR2Credentials, - getR2S3Client, getS3Client, uploadToBucket, } diff --git a/ironfish-rust-nodejs/Cargo.toml b/ironfish-rust-nodejs/Cargo.toml index 2ef08f563a..e3c19ad437 100644 --- a/ironfish-rust-nodejs/Cargo.toml +++ b/ironfish-rust-nodejs/Cargo.toml @@ -30,7 +30,6 @@ base64 = "0.13.0" fish_hash = "0.3.0" ironfish = { path = "../ironfish-rust" } ironfish-frost = { git = "https://github.com/iron-fish/ironfish-frost.git", branch = "main" } -ironfish_mpc = { path = "../ironfish-mpc" } napi = { version = "2.13.2", features = ["napi6"] } napi-derive = "2.13.0" jubjub = { git = "https://github.com/iron-fish/jubjub.git", branch = "blstrs" } diff --git a/ironfish-rust-nodejs/src/lib.rs b/ironfish-rust-nodejs/src/lib.rs index ca77a59134..ab10bca085 100644 --- a/ironfish-rust-nodejs/src/lib.rs +++ b/ironfish-rust-nodejs/src/lib.rs @@ -16,7 +16,6 @@ use ironfish::mining; use ironfish::sapling_bls12; pub mod fish_hash; -pub mod mpc; pub mod multisig; pub mod nacl; pub mod rolling_filter; diff --git a/ironfish-rust-nodejs/src/mpc.rs b/ironfish-rust-nodejs/src/mpc.rs deleted file mode 100644 index 657ff58115..0000000000 --- a/ironfish-rust-nodejs/src/mpc.rs +++ /dev/null @@ -1,75 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -use ironfish_mpc; - -use napi::bindgen_prelude::*; -use napi::{Env, JsString, Result, Task}; -use napi_derive::napi; - -use crate::to_napi_err; - -pub struct Contribute { - input_path: String, - output_path: String, - seed: Option, -} - -#[napi] -impl Task for Contribute { - type Output = String; - type JsValue = JsString; - - fn compute(&mut self) -> Result { - ironfish_mpc::compute(&self.input_path, &self.output_path, &self.seed).map_err(to_napi_err) - } - - fn resolve(&mut self, env: Env, output: Self::Output) -> Result { - env.create_string(&output) - } -} - -#[napi] -pub fn contribute( - input_path: String, - output_path: String, - seed: Option, -) -> AsyncTask { - AsyncTask::new(Contribute { - input_path, - output_path, - seed, - }) -} - -pub struct VerifyTransform { - params_path: String, - new_params_path: String, -} - -#[napi] -impl Task for VerifyTransform { - type Output = String; - type JsValue = JsString; - - fn compute(&mut self) -> Result { - ironfish_mpc::verify_transform(&self.params_path, &self.new_params_path) - .map_err(to_napi_err) - } - - fn resolve(&mut self, env: Env, output: Self::Output) -> Result { - env.create_string(&output) - } -} - -#[napi] -pub fn verify_transform( - params_path: String, - new_params_path: String, -) -> AsyncTask { - AsyncTask::new(VerifyTransform { - params_path, - new_params_path, - }) -} From 4c54d96e40ddf6476e4433286a6c777662d609a7 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Thu, 18 Apr 2024 16:04:31 -0500 Subject: [PATCH 12/35] Standardizing post CLI command (#4911) Adds a -f flag to specify the account the transaction is being sent from The raw transaction input is not optional and can be entered using a long prompt --- ironfish-cli/src/commands/wallet/post.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/post.ts b/ironfish-cli/src/commands/wallet/post.ts index 69b862c4b1..4235410d19 100644 --- a/ironfish-cli/src/commands/wallet/post.ts +++ b/ironfish-cli/src/commands/wallet/post.ts @@ -11,6 +11,7 @@ import { import { CliUx, Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' +import { longPrompt } from '../../utils/longPrompt' export class PostCommand extends IronfishCommand { static summary = 'Post a raw transaction' @@ -26,6 +27,7 @@ export class PostCommand extends IronfishCommand { ...RemoteFlags, account: Flags.string({ description: 'The account that created the raw transaction', + char: 'f', required: false, }), confirm: Flags.boolean({ @@ -42,14 +44,19 @@ export class PostCommand extends IronfishCommand { static args = [ { name: 'transaction', - required: true, description: 'The raw transaction in hex encoding', }, ] async start(): Promise { const { flags, args } = await this.parse(PostCommand) - const transaction = args.transaction as string + let transaction = args.transaction as string | undefined + + if (!transaction) { + transaction = await longPrompt('Enter the raw transaction in hex encoding', { + required: true, + }) + } const serialized = Buffer.from(transaction, 'hex') const raw = RawTransactionSerde.deserialize(serialized) From 45a973e5d4b33df6bb34ec0b6e30fb34fdf7e446 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Thu, 18 Apr 2024 17:30:52 -0400 Subject: [PATCH 13/35] feat(cli,ironfish): Add flag to override name in round3 (#4912) * feat(cli,ironfish): Add flag to override name in round3 * Rename to accountName * Rename cli flag to accountName --- .../src/commands/wallet/multisig/dkg/round3.ts | 5 +++++ .../rpc/routes/wallet/multisig/dkg/round3.test.ts | 14 +++++++++++++- .../src/rpc/routes/wallet/multisig/dkg/round3.ts | 4 +++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts index 84257e52f7..fadc6b15c6 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -17,6 +17,10 @@ export class DkgRound3Command extends IronfishCommand { description: 'The name of the secret to use for decryption during DKG', required: true, }), + accountName: Flags.string({ + char: 'n', + description: 'The name to set for the imported account', + }), round2SecretPackage: Flags.string({ char: 'e', description: 'The encrypted secret package created during DKG round2', @@ -88,6 +92,7 @@ export class DkgRound3Command extends IronfishCommand { const response = await client.wallet.multisig.dkg.round3({ secretName: flags.secretName, + accountName: flags.accountName, round2SecretPackage, round1PublicPackages, round2PublicPackages, diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts index 678b84cbca..9d8d2dbfd4 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts @@ -48,11 +48,18 @@ describe('Route multisig/dkg/round3', () => { ), ) + // Only override 2/3 names + const secretNamesToName = { + [secretNames[0]]: 'foo', + [secretNames[2]]: 'bar', + } + // Perform DKG round 3 const round3Responses = await Promise.all( secretNames.map((secretName, index) => routeTest.client.wallet.multisig.dkg.round3({ secretName, + accountName: secretNamesToName[secretName], round2SecretPackage: round2Packages[index].content.encryptedSecretPackage, round1PublicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), round2PublicPackages: round2Packages.flatMap((pkg) => @@ -71,7 +78,9 @@ describe('Route multisig/dkg/round3', () => { secretNames.map( async (account) => ( - await routeTest.client.wallet.getAccountPublicKey({ account }) + await routeTest.client.wallet.getAccountPublicKey({ + account: secretNamesToName[account] ?? account, + }) ).content.publicKey, ), ) @@ -83,6 +92,9 @@ describe('Route multisig/dkg/round3', () => { // Check all the responses match expect(round3Responses).toHaveLength(publicKeys.length) for (let i = 0; i < round3Responses.length; i++) { + expect(round3Responses[i].content.name).toEqual( + secretNamesToName[secretNames[i]] ?? secretNames[i], + ) expect(round3Responses[i].content.publicAddress).toEqual(publicKeys[i]) } }) diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts index ce97b13a27..dad6e3e2c1 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts @@ -15,6 +15,7 @@ export type DkgRound3Request = { round2SecretPackage: string round1PublicPackages: Array round2PublicPackages: Array + accountName?: string } export type DkgRound3Response = { @@ -28,6 +29,7 @@ export const DkgRound3RequestSchema: yup.ObjectSchema = yup round2SecretPackage: yup.string().defined(), round1PublicPackages: yup.array().of(yup.string().defined()).defined(), round2PublicPackages: yup.array().of(yup.string().defined()).defined(), + accountName: yup.string().optional(), }) .defined() @@ -74,7 +76,7 @@ routes.register( ) const accountImport = { - name: secretName, + name: request.data.accountName ?? secretName, version: ACCOUNT_SCHEMA_VERSION, createdAt: null, spendingKey: null, From 3defd8bd8d9355182b7b2bcc1eb3e6c7b7b8a795 Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Sat, 20 Apr 2024 12:48:40 -0400 Subject: [PATCH 14/35] Move ironfish-mpc to a new repository (#4914) The ironfish-mpc and ironfish-phase2 projects are now hosted in this repository: https://github.com/iron-fish/ironfish-mpc --- Cargo.lock | 120 +- Cargo.toml | 5 - ironfish-mpc/.gitignore | 4 - ironfish-mpc/COPYRIGHT | 14 - ironfish-mpc/Cargo.toml | 55 - ironfish-mpc/LICENSE-APACHE | 201 --- ironfish-mpc/LICENSE-MIT | 23 - ironfish-mpc/README.md | 35 - ironfish-mpc/src/bin/beacon.rs | 93 -- ironfish-mpc/src/bin/compute.rs | 17 - ironfish-mpc/src/bin/new.rs | 49 - ironfish-mpc/src/bin/split_params.rs | 48 - ironfish-mpc/src/bin/verify.rs | 76 -- ironfish-mpc/src/bin/verify_transform.rs | 7 - ironfish-mpc/src/compute.rs | 47 - ironfish-mpc/src/lib.rs | 5 - ironfish-mpc/src/verify_transform.rs | 42 - ironfish-phase2/.gitignore | 4 - ironfish-phase2/COPYRIGHT | 14 - ironfish-phase2/Cargo.toml | 25 - ironfish-phase2/LICENSE-APACHE | 201 --- ironfish-phase2/LICENSE-MIT | 23 - ironfish-phase2/README.md | 19 - ironfish-phase2/src/lib.rs | 1438 ---------------------- ironfish-rust-nodejs/index.d.ts | 2 - ironfish-rust-nodejs/index.js | 4 +- supply-chain/config.toml | 31 - supply-chain/imports.lock | 6 - 28 files changed, 4 insertions(+), 2604 deletions(-) delete mode 100644 ironfish-mpc/.gitignore delete mode 100644 ironfish-mpc/COPYRIGHT delete mode 100644 ironfish-mpc/Cargo.toml delete mode 100644 ironfish-mpc/LICENSE-APACHE delete mode 100644 ironfish-mpc/LICENSE-MIT delete mode 100644 ironfish-mpc/README.md delete mode 100644 ironfish-mpc/src/bin/beacon.rs delete mode 100644 ironfish-mpc/src/bin/compute.rs delete mode 100644 ironfish-mpc/src/bin/new.rs delete mode 100644 ironfish-mpc/src/bin/split_params.rs delete mode 100644 ironfish-mpc/src/bin/verify.rs delete mode 100644 ironfish-mpc/src/bin/verify_transform.rs delete mode 100644 ironfish-mpc/src/compute.rs delete mode 100644 ironfish-mpc/src/lib.rs delete mode 100644 ironfish-mpc/src/verify_transform.rs delete mode 100644 ironfish-phase2/.gitignore delete mode 100644 ironfish-phase2/COPYRIGHT delete mode 100644 ironfish-phase2/Cargo.toml delete mode 100644 ironfish-phase2/LICENSE-APACHE delete mode 100644 ironfish-phase2/LICENSE-MIT delete mode 100644 ironfish-phase2/README.md delete mode 100644 ironfish-phase2/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 2647fd64c9..a5739e7477 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,26 +122,6 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" -[[package]] -name = "bellman" -version = "0.13.1" -source = "git+https://github.com/iron-fish/bellman?rev=1cc52ca33e6db14233f1cbc0c9c5b7c822b229ec#1cc52ca33e6db14233f1cbc0c9c5b7c822b229ec" -dependencies = [ - "bitvec", - "blake2s_simd", - "byteorder", - "crossbeam-channel", - "ff 0.12.1", - "group 0.12.1", - "lazy_static", - "log", - "num_cpus", - "pairing 0.22.0", - "rand_core", - "rayon", - "subtle", -] - [[package]] name = "bellperson" version = "0.24.1" @@ -159,7 +139,7 @@ dependencies = [ "group 0.12.1", "log", "memmap2", - "pairing 0.22.0", + "pairing", "rand", "rand_core", "rayon", @@ -224,15 +204,6 @@ dependencies = [ "wyz", ] -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest 0.10.6", -] - [[package]] name = "blake2b_simd" version = "1.0.0" @@ -309,8 +280,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3c196a77437e7cc2fb515ce413a6401291578b5afc8ecb29a3c7ab957f05941" dependencies = [ "ff 0.12.1", - "group 0.12.1", - "pairing 0.22.0", "rand_core", "subtle", ] @@ -349,7 +318,7 @@ dependencies = [ "byte-slice-cast", "ff 0.12.1", "group 0.12.1", - "pairing 0.22.0", + "pairing", "rand_core", "serde", "subtle", @@ -1342,31 +1311,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hex-literal" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc2928beef125e519d69ae1baa8c37ea2e0d3848545217f6db0179c5eb1d639" -dependencies = [ - "hex-literal-impl", - "proc-macro-hack", -] - [[package]] name = "hex-literal" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" -[[package]] -name = "hex-literal-impl" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520870c3213943eb8d7803e80180d12a6c7ceb4ae74602544529d1643dc4ddda" -dependencies = [ - "proc-macro-hack", -] - [[package]] name = "hmac" version = "0.11.0" @@ -1527,7 +1477,7 @@ dependencies = [ "fish_hash", "group 0.12.1", "hex", - "hex-literal 0.4.1", + "hex-literal", "ironfish-frost", "ironfish_zkp", "jubjub 0.9.0 (git+https://github.com/iron-fish/jubjub.git?branch=blstrs)", @@ -1556,22 +1506,6 @@ dependencies = [ "x25519-dalek", ] -[[package]] -name = "ironfish-phase2" -version = "0.2.2" -dependencies = [ - "bellman", - "blake2", - "bls12_381 0.7.1", - "byteorder", - "ff 0.12.1", - "group 0.12.1", - "pairing 0.22.0", - "rand", - "rand_chacha", - "rayon", -] - [[package]] name = "ironfish-rust-nodejs" version = "0.1.0" @@ -1587,21 +1521,6 @@ dependencies = [ "rand", ] -[[package]] -name = "ironfish_mpc" -version = "0.2.0" -dependencies = [ - "blake2", - "byteorder", - "hex-literal 0.1.4", - "ironfish-phase2", - "ironfish_zkp", - "pairing 0.23.0", - "rand", - "rand_chacha", - "rand_seeder", -] - [[package]] name = "ironfish_zkp" version = "0.2.0" @@ -2030,15 +1949,6 @@ dependencies = [ "group 0.12.1", ] -[[package]] -name = "pairing" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" -dependencies = [ - "group 0.13.0", -] - [[package]] name = "password-hash" version = "0.3.2" @@ -2204,21 +2114,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" -[[package]] -name = "proc-macro-hack" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7f95648580798cc44ff8efb9bb0d7ee5205ea32e087b31b0732f3e8c2648ee2" -dependencies = [ - "proc-macro-hack-impl", -] - -[[package]] -name = "proc-macro-hack-impl" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be55bf0ae1635f4d7c7ddd6efc05c631e98a82104a73d35550bbc52db960027" - [[package]] name = "proc-macro2" version = "1.0.60" @@ -2273,15 +2168,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rand_seeder" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2890aaef0aa82719a50e808de264f9484b74b442e1a3a0e5ee38243ac40bdb" -dependencies = [ - "rand_core", -] - [[package]] name = "rand_xorshift" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index db2ec00905..7ceea9ac2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,8 +3,6 @@ resolver = "2" members = [ "benchmarks", - "ironfish-mpc", - "ironfish-phase2", "ironfish-rust", "ironfish-rust-nodejs", "ironfish-zkp", @@ -16,8 +14,5 @@ edition = "2021" homepage = "https://ironfish.network/" repository = "https://github.com/iron-fish/ironfish" -[patch.crates-io] -bellman = { git = "https://github.com/iron-fish/bellman", rev = "1cc52ca33e6db14233f1cbc0c9c5b7c822b229ec" } - [profile.release] debug = true \ No newline at end of file diff --git a/ironfish-mpc/.gitignore b/ironfish-mpc/.gitignore deleted file mode 100644 index 4bcaac6033..0000000000 --- a/ironfish-mpc/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -phase1* -params -new_params -*.params diff --git a/ironfish-mpc/COPYRIGHT b/ironfish-mpc/COPYRIGHT deleted file mode 100644 index 0b7995e7dd..0000000000 --- a/ironfish-mpc/COPYRIGHT +++ /dev/null @@ -1,14 +0,0 @@ -Copyrights in the "sapling-mpc" library are retained by their contributors. No -copyright assignment is required to contribute to the "sapling-mpc" library. - -The "sapling-mpc" library is licensed under either of - - * Apache License, Version 2.0, (see ./LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) - * MIT license (see ./LICENSE-MIT or http://opensource.org/licenses/MIT) - -at your option. - -Unless you explicitly state otherwise, any contribution intentionally -submitted for inclusion in the work by you, as defined in the Apache-2.0 -license, shall be dual licensed as above, without any additional terms or -conditions. \ No newline at end of file diff --git a/ironfish-mpc/Cargo.toml b/ironfish-mpc/Cargo.toml deleted file mode 100644 index cba5ed8d0c..0000000000 --- a/ironfish-mpc/Cargo.toml +++ /dev/null @@ -1,55 +0,0 @@ -[package] -name = "ironfish_mpc" -version = "0.2.0" -authors = ["Sean Bowe ", "Iron Fish (https://ironfish.network)"] - -publish = false - -[package.edition] -workspace = true - -[[bin]] -name = "new" -required-features = ["verification"] - -[[bin]] -name = "compute" - -[[bin]] -name = "verify" -required-features = ["verification"] - -[[bin]] -name = "verify_transform" -required-features = ["verification"] - -[[bin]] -name = "beacon" -required-features = ["beacon"] - -[[bin]] -name = "split_params" - -[dependencies] -ironfish-phase2 = { path = "../ironfish-phase2" } -pairing = "0.23" -rand = "0.8.5" -rand_chacha = "0.3.1" -rand_seeder = "0.2.3" -blake2 = "0.10.6" - -[dependencies.ironfish_zkp] -path = "../ironfish-zkp" -optional = true - -[dependencies.byteorder] -version = "1.5" -optional = true - -[dependencies.hex-literal] -version = "0.1" -optional = true - -[features] -verification = ["ironfish_zkp"] -beacon = ["byteorder"] diff --git a/ironfish-mpc/LICENSE-APACHE b/ironfish-mpc/LICENSE-APACHE deleted file mode 100644 index 16fe87b06e..0000000000 --- a/ironfish-mpc/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/ironfish-mpc/LICENSE-MIT b/ironfish-mpc/LICENSE-MIT deleted file mode 100644 index 31aa79387f..0000000000 --- a/ironfish-mpc/LICENSE-MIT +++ /dev/null @@ -1,23 +0,0 @@ -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. diff --git a/ironfish-mpc/README.md b/ironfish-mpc/README.md deleted file mode 100644 index 32da1e16b7..0000000000 --- a/ironfish-mpc/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# ironfish-mpc - -Much of the code in this folder was originally forked from https://github.com/zcash-hackworks/sapling-mpc. The original licenses and copyright are retained in this folder. - -## Beacon - -Our final contribution will be seeded using the randomness generated from [The League of Entropy's drand network](https://drand.love/) in round #2,863,343 (Wed Apr 12th ~1:30 PDT). - -The results of Drand's round 2,863,343 will be listed below - -From: https://api.drand.sh/8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce/public/2863343 -```json -{ - "round": 2863343, - "randomness": "32e360d600ece16bc0c4248eb5f3a355b4af5fefe978530480113b522c09d36c", - "signature": "823cfda3099e515022253b76e3a8ee43e0b9989b56d8aaff31d976c0dde6ba2bafc2cbd4c84d6377deef7e8bb21cb53d15af8beb1480b1ec2e541ca4bd08bc1252e7c7922256445a3b32717bb38ec894eee8017ff67218c5dbfa81576e1cf134", - "previous_signature": "a96719eb694b01dcecf6b38bae832ba425774ea35d8359f544937aad0022ca8b5fdc517fbd013f12df9ffe89c60329b5184eb8b8582b316e946ac640f2b0a3ad0c06911c0c891fb948ce9ea398f4c8b1d20195990ccbb51d75810ca7a7ee1e45" -} -``` - -## License - -Licensed under either of - - * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) - * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) - -at your option. - -### Contribution - -Unless you explicitly state otherwise, any contribution intentionally -submitted for inclusion in the work by you, as defined in the Apache-2.0 -license, shall be dual licensed as above, without any additional terms or -conditions. diff --git a/ironfish-mpc/src/bin/beacon.rs b/ironfish-mpc/src/bin/beacon.rs deleted file mode 100644 index 6672395eff..0000000000 --- a/ironfish-mpc/src/bin/beacon.rs +++ /dev/null @@ -1,93 +0,0 @@ -extern crate pairing; -extern crate rand; -extern crate rand_chacha; - -use blake2::{Blake2b512, Digest}; -use std::convert::TryInto; -use std::fs::File; -use std::io::{BufReader, BufWriter}; - -fn decode_hex(s: &str) -> Vec { - (0..s.len()) - .step_by(2) - .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()) - .collect() -} - -fn main() { - let current_params = File::open("params").expect("couldn't open `./params`"); - let mut current_params = BufReader::with_capacity(1024 * 1024, current_params); - - let new_params = File::create("new_params").expect("couldn't create `./new_params`"); - let mut new_params = BufWriter::with_capacity(1024 * 1024, new_params); - - let mut sapling_spend = ironfish_phase2::MPCParameters::read(&mut current_params, false) - .expect("couldn't deserialize Sapling Spend params"); - - let mut sapling_output = ironfish_phase2::MPCParameters::read(&mut current_params, false) - .expect("couldn't deserialize Sapling Output params"); - - let mut sapling_mint = ironfish_phase2::MPCParameters::read(&mut current_params, false) - .expect("couldn't deserialize Sapling Mint params"); - - // Create an RNG based on the outcome of the random beacon - let rng = &mut { - use rand::SeedableRng; - use rand_chacha::ChaChaRng; - - // Place beacon value here. The value will be the randomness generated by The League of Entropy's drand network - // (network chain hash: 8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce) in round #2863343. - let beacon_value: [u8; 32] = - decode_hex("32e360d600ece16bc0c4248eb5f3a355b4af5fefe978530480113b522c09d36c") - .as_slice() - .try_into() - .unwrap(); - - print!("Final result of beacon: "); - for b in beacon_value.iter() { - print!("{:02x}", b); - } - println!(); - - ChaChaRng::from_seed(beacon_value) - }; - - let h1 = sapling_spend.contribute(rng); - let h2 = sapling_output.contribute(rng); - let h3 = sapling_mint.contribute(rng); - - sapling_spend - .write(&mut new_params) - .expect("couldn't write new Sapling Spend params"); - sapling_output - .write(&mut new_params) - .expect("couldn't write new Sapling Output params"); - sapling_mint - .write(&mut new_params) - .expect("couldn't write new Sapling Mint params"); - - let mut h = Blake2b512::new(); - h.update(h1); - h.update(h2); - h.update(h3); - let h = h.finalize(); - - print!( - "Done!\n\n\ - Your contribution has been written to `./new_params`\n\n\ - The contribution you made is bound to the following hash:\n" - ); - - for line in h.chunks(16) { - print!("\t"); - for section in line.chunks(4) { - for b in section { - print!("{:02x}", b); - } - print!(" "); - } - println!(); - } - - println!("\n"); -} diff --git a/ironfish-mpc/src/bin/compute.rs b/ironfish-mpc/src/bin/compute.rs deleted file mode 100644 index 9a58ec0f0e..0000000000 --- a/ironfish-mpc/src/bin/compute.rs +++ /dev/null @@ -1,17 +0,0 @@ -use ironfish_mpc::compute; - -fn main() { - let hash = compute("params", "new_params", &None).unwrap(); - - println!("{}", into_hex(hash.as_ref())); -} - -fn into_hex(h: &[u8]) -> String { - let mut f = String::new(); - - for byte in h { - f += &format!("{:02x}", byte); - } - - f -} diff --git a/ironfish-mpc/src/bin/new.rs b/ironfish-mpc/src/bin/new.rs deleted file mode 100644 index e5c73df64c..0000000000 --- a/ironfish-mpc/src/bin/new.rs +++ /dev/null @@ -1,49 +0,0 @@ -extern crate pairing; - -use std::fs::File; -use std::io::BufWriter; - -use ironfish_zkp::constants::ASSET_ID_LENGTH; - -fn main() { - let params = File::create("params").unwrap(); - let mut params = BufWriter::with_capacity(1024 * 1024, params); - - // Sapling spend circuit - ironfish_phase2::MPCParameters::new(ironfish_zkp::proofs::Spend { - value_commitment: None, - proof_generation_key: None, - payment_address: None, - commitment_randomness: None, - ar: None, - auth_path: vec![None; ironfish_zkp::constants::TREE_DEPTH], - anchor: None, - sender_address: None, - }) - .unwrap() - .write(&mut params) - .unwrap(); - - // Sapling output circuit - ironfish_phase2::MPCParameters::new(ironfish_zkp::proofs::Output { - value_commitment: None, - payment_address: None, - commitment_randomness: None, - esk: None, - asset_id: [0; ASSET_ID_LENGTH], - ar: None, - proof_generation_key: None, - }) - .unwrap() - .write(&mut params) - .unwrap(); - - // Sapling mint circuit - ironfish_phase2::MPCParameters::new(ironfish_zkp::proofs::MintAsset { - proof_generation_key: None, - public_key_randomness: None, - }) - .unwrap() - .write(&mut params) - .unwrap(); -} diff --git a/ironfish-mpc/src/bin/split_params.rs b/ironfish-mpc/src/bin/split_params.rs deleted file mode 100644 index f475bcc710..0000000000 --- a/ironfish-mpc/src/bin/split_params.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! This binary just splits the parameters up into separate files. - -extern crate pairing; -extern crate rand; - -use std::fs::File; -use std::io::{BufReader, BufWriter}; - -fn main() { - let current_params = File::open("params").expect("couldn't open `./params`"); - let mut current_params = BufReader::with_capacity(1024 * 1024, current_params); - - let sapling_spend = ironfish_phase2::MPCParameters::read(&mut current_params, false) - .expect("couldn't deserialize Sapling Spend params"); - - let sapling_output = ironfish_phase2::MPCParameters::read(&mut current_params, false) - .expect("couldn't deserialize Sapling Output params"); - - let sapling_mint = ironfish_phase2::MPCParameters::read(&mut current_params, false) - .expect("couldn't deserialize Sapling Mint params"); - - { - let f = - File::create("sapling-spend.params").expect("couldn't create `./sapling-spend.params`"); - let mut f = BufWriter::with_capacity(1024 * 1024, f); - sapling_spend - .write(&mut f) - .expect("couldn't write new Sapling Spend params"); - } - - { - let f = File::create("sapling-output.params") - .expect("couldn't create `./sapling-output.params`"); - let mut f = BufWriter::with_capacity(1024 * 1024, f); - sapling_output - .write(&mut f) - .expect("couldn't write new Sapling Output params"); - } - - { - let f = - File::create("sapling-mint.params").expect("couldn't create `./sapling-mint.params`"); - let mut f = BufWriter::with_capacity(1024 * 1024, f); - sapling_mint - .write(&mut f) - .expect("couldn't write new Sapling Mint params"); - } -} diff --git a/ironfish-mpc/src/bin/verify.rs b/ironfish-mpc/src/bin/verify.rs deleted file mode 100644 index 26d732568a..0000000000 --- a/ironfish-mpc/src/bin/verify.rs +++ /dev/null @@ -1,76 +0,0 @@ -extern crate pairing; - -use blake2::{Blake2b512, Digest}; -use ironfish_zkp::constants::ASSET_ID_LENGTH; -use std::fs::File; -use std::io::BufReader; - -fn main() { - let params = File::open("params").unwrap(); - let mut params = BufReader::with_capacity(1024 * 1024, params); - - let sapling_spend = ironfish_phase2::MPCParameters::read(&mut params, true) - .expect("couldn't deserialize Sapling Spend params"); - - let sapling_output = ironfish_phase2::MPCParameters::read(&mut params, true) - .expect("couldn't deserialize Sapling Output params"); - - let sapling_mint = ironfish_phase2::MPCParameters::read(&mut params, true) - .expect("couldn't deserialize Sapling Mint params"); - - let sapling_spend_contributions = sapling_spend - .verify(ironfish_zkp::proofs::Spend { - value_commitment: None, - proof_generation_key: None, - payment_address: None, - commitment_randomness: None, - ar: None, - auth_path: vec![None; ironfish_zkp::constants::TREE_DEPTH], - anchor: None, - sender_address: None, - }) - .expect("parameters are invalid"); - - let sapling_output_contributions = sapling_output - .verify(ironfish_zkp::proofs::Output { - value_commitment: None, - payment_address: None, - commitment_randomness: None, - esk: None, - asset_id: [0; ASSET_ID_LENGTH], - ar: None, - proof_generation_key: None, - }) - .expect("parameters are invalid"); - - let sapling_mint_contributions = sapling_mint - .verify(ironfish_zkp::proofs::MintAsset { - proof_generation_key: None, - public_key_randomness: None, - }) - .expect("parameters are invalid"); - - for ((a, b), c) in sapling_spend_contributions - .into_iter() - .zip(sapling_output_contributions.into_iter()) - .zip(sapling_mint_contributions) - { - let mut h = Blake2b512::new(); - h.update(a); - h.update(b); - h.update(c); - let h = h.finalize(); - - println!("{}", into_hex(h.as_ref())); - } -} - -fn into_hex(h: &[u8]) -> String { - let mut f = String::new(); - - for byte in h { - f += &format!("{:02x}", byte); - } - - f -} diff --git a/ironfish-mpc/src/bin/verify_transform.rs b/ironfish-mpc/src/bin/verify_transform.rs deleted file mode 100644 index 24a8f32eba..0000000000 --- a/ironfish-mpc/src/bin/verify_transform.rs +++ /dev/null @@ -1,7 +0,0 @@ -use ironfish_mpc::verify_transform; - -fn main() { - let hash = verify_transform("params", "new_params").unwrap(); - - println!("{}", hash); -} diff --git a/ironfish-mpc/src/compute.rs b/ironfish-mpc/src/compute.rs deleted file mode 100644 index aa1c0a872a..0000000000 --- a/ironfish-mpc/src/compute.rs +++ /dev/null @@ -1,47 +0,0 @@ -extern crate pairing; -extern crate rand; - -use blake2::{Blake2b512, Digest}; -use rand_chacha::ChaCha20Rng; -use rand_seeder::Seeder; -use std::fs::File; -use std::io::{BufReader, BufWriter}; - -pub fn compute( - input_path: &str, - output_path: &str, - seed: &Option, -) -> Result { - let current_params = File::open(input_path)?; - let mut current_params = BufReader::with_capacity(1024 * 1024, current_params); - - let new_params = File::create(output_path)?; - let mut new_params = BufWriter::with_capacity(1024 * 1024, new_params); - - let mut sapling_spend = ironfish_phase2::MPCParameters::read(&mut current_params, false)?; - - let mut sapling_output = ironfish_phase2::MPCParameters::read(&mut current_params, false)?; - - let mut sapling_mint = ironfish_phase2::MPCParameters::read(&mut current_params, false)?; - - let rng: &mut Box = &mut match seed { - Some(s) => Box::new(Seeder::from(s).make_rng::()), - None => Box::new(rand::thread_rng()), - }; - - let h1 = sapling_spend.contribute(rng); - let h2 = sapling_output.contribute(rng); - let h3 = sapling_mint.contribute(rng); - - sapling_spend.write(&mut new_params)?; - sapling_output.write(&mut new_params)?; - sapling_mint.write(&mut new_params)?; - - let mut h = Blake2b512::new(); - h.update(h1); - h.update(h2); - h.update(h3); - let h = h.finalize(); - - Ok(format!("{:02x}", h)) -} diff --git a/ironfish-mpc/src/lib.rs b/ironfish-mpc/src/lib.rs deleted file mode 100644 index 95f08cbeca..0000000000 --- a/ironfish-mpc/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod compute; -mod verify_transform; - -pub use compute::compute; -pub use verify_transform::verify_transform; diff --git a/ironfish-mpc/src/verify_transform.rs b/ironfish-mpc/src/verify_transform.rs deleted file mode 100644 index 44bcb8f735..0000000000 --- a/ironfish-mpc/src/verify_transform.rs +++ /dev/null @@ -1,42 +0,0 @@ -extern crate pairing; - -use blake2::{Blake2b512, Digest}; -use std::fs::File; -use std::io::BufReader; - -pub fn verify_transform( - params_path: &str, - new_params_path: &str, -) -> Result { - let params = File::open(params_path)?; - let mut params = BufReader::with_capacity(1024 * 1024, params); - - let new_params = File::open(new_params_path)?; - let mut new_params = BufReader::with_capacity(1024 * 1024, new_params); - - let sapling_spend = ironfish_phase2::MPCParameters::read(&mut params, false)?; - - let sapling_output = ironfish_phase2::MPCParameters::read(&mut params, false)?; - - let sapling_mint = ironfish_phase2::MPCParameters::read(&mut params, false)?; - - let new_sapling_spend = ironfish_phase2::MPCParameters::read(&mut new_params, true)?; - - let new_sapling_output = ironfish_phase2::MPCParameters::read(&mut new_params, true)?; - - let new_sapling_mint = ironfish_phase2::MPCParameters::read(&mut new_params, true)?; - - let h1 = ironfish_phase2::verify_contribution(&sapling_spend, &new_sapling_spend)?; - - let h2 = ironfish_phase2::verify_contribution(&sapling_output, &new_sapling_output)?; - - let h3 = ironfish_phase2::verify_contribution(&sapling_mint, &new_sapling_mint)?; - - let mut h = Blake2b512::new(); - h.update(h1); - h.update(h2); - h.update(h3); - let h = h.finalize(); - - Ok(format!("{:02x}", h)) -} diff --git a/ironfish-phase2/.gitignore b/ironfish-phase2/.gitignore deleted file mode 100644 index 1e988784c3..0000000000 --- a/ironfish-phase2/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -phase1* -/target/ -**/*.rs.bk -Cargo.lock diff --git a/ironfish-phase2/COPYRIGHT b/ironfish-phase2/COPYRIGHT deleted file mode 100644 index 3b6df59863..0000000000 --- a/ironfish-phase2/COPYRIGHT +++ /dev/null @@ -1,14 +0,0 @@ -Copyrights in the "phase2" library are retained by their contributors. No -copyright assignment is required to contribute to the "phase2" library. - -The "phase2" library is licensed under either of - - * Apache License, Version 2.0, (see ./LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) - * MIT license (see ./LICENSE-MIT or http://opensource.org/licenses/MIT) - -at your option. - -Unless you explicitly state otherwise, any contribution intentionally -submitted for inclusion in the work by you, as defined in the Apache-2.0 -license, shall be dual licensed as above, without any additional terms or -conditions. diff --git a/ironfish-phase2/Cargo.toml b/ironfish-phase2/Cargo.toml deleted file mode 100644 index c9980d32c1..0000000000 --- a/ironfish-phase2/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "ironfish-phase2" -version = "0.2.2" -authors = ["Sean Bowe ", "Iron Fish (https://ironfish.network)"] -description = "Library for performing MPCs for creating zk-SNARK public parameters" -homepage = "https://github.com/iron-fish/ironfish" -license = "MIT OR Apache-2.0" -repository = "https://github.com/iron-fish/ironfish" - -publish = false - -[package.edition] -workspace = true - -[dependencies] -pairing = "0.22.0" -rand = "0.8.5" -rand_chacha = "0.3.1" -rayon = "1.6.1" -bellman = "0.13.1" -bls12_381 = "0.7.0" -ff = "0.12.0" -group = "0.12.0" -byteorder = "1" -blake2 = "0.10.6" diff --git a/ironfish-phase2/LICENSE-APACHE b/ironfish-phase2/LICENSE-APACHE deleted file mode 100644 index 16fe87b06e..0000000000 --- a/ironfish-phase2/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/ironfish-phase2/LICENSE-MIT b/ironfish-phase2/LICENSE-MIT deleted file mode 100644 index 31aa79387f..0000000000 --- a/ironfish-phase2/LICENSE-MIT +++ /dev/null @@ -1,23 +0,0 @@ -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. diff --git a/ironfish-phase2/README.md b/ironfish-phase2/README.md deleted file mode 100644 index 026aae3073..0000000000 --- a/ironfish-phase2/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# ironfish-phase2 - -Much of the code in this folder was originally forked from https://github.com/ebfull/phase2. The original licenses and copyright are retained in this folder. - -## License - -Licensed under either of - - * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) - * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) - -at your option. - -### Contribution - -Unless you explicitly state otherwise, any contribution intentionally -submitted for inclusion in the work by you, as defined in the Apache-2.0 -license, shall be dual licensed as above, without any additional terms or -conditions. diff --git a/ironfish-phase2/src/lib.rs b/ironfish-phase2/src/lib.rs deleted file mode 100644 index 5c12fe1c23..0000000000 --- a/ironfish-phase2/src/lib.rs +++ /dev/null @@ -1,1438 +0,0 @@ -//! # zk-SNARK MPCs, made easy. -//! -//! ## Make your circuit -//! -//! Grab the [`bellman`](https://github.com/ebfull/bellman) and -//! [`pairing`](https://github.com/ebfull/pairing) crates. Bellman -//! provides a trait called `Circuit`, which you must implement -//! for your computation. -//! -//! Here's a silly example: proving you know the cube root of -//! a field element. -//! -//! ```rust -//! extern crate pairing; -//! extern crate bellman; -//! -//! use pairing::{Engine, Field}; -//! use bellman::{ -//! Circuit, -//! ConstraintSystem, -//! SynthesisError, -//! }; -//! -//! struct CubeRoot { -//! cube_root: Option -//! } -//! -//! impl Circuit for CubeRoot { -//! fn synthesize>( -//! self, -//! cs: &mut CS -//! ) -> Result<(), SynthesisError> -//! { -//! // Witness the cube root -//! let root = cs.alloc(|| "root", || { -//! self.cube_root.ok_or(SynthesisError::AssignmentMissing) -//! })?; -//! -//! // Witness the square of the cube root -//! let square = cs.alloc(|| "square", || { -//! self.cube_root -//! .ok_or(SynthesisError::AssignmentMissing) -//! .map(|mut root| {root.square(); root }) -//! })?; -//! -//! // Enforce that `square` is root^2 -//! cs.enforce( -//! || "squaring", -//! |lc| lc + root, -//! |lc| lc + root, -//! |lc| lc + square -//! ); -//! -//! // Witness the cube, as a public input -//! let cube = cs.alloc_input(|| "cube", || { -//! self.cube_root -//! .ok_or(SynthesisError::AssignmentMissing) -//! .map(|root| { -//! let mut tmp = root; -//! tmp.square(); -//! tmp.mul_assign(&root); -//! tmp -//! }) -//! })?; -//! -//! // Enforce that `cube` is root^3 -//! // i.e. that `cube` is `root` * `square` -//! cs.enforce( -//! || "cubing", -//! |lc| lc + root, -//! |lc| lc + square, -//! |lc| lc + cube -//! ); -//! -//! Ok(()) -//! } -//! } -//! ``` -//! -//! ## Create some proofs -//! -//! Now that we have `CubeRoot` implementing `Circuit`, -//! let's create some parameters and make some proofs. -//! -//! ```rust,ignore -//! extern crate rand; -//! -//! use pairing::bls12_381::{Bls12, Fr}; -//! use bellman::groth16::{ -//! generate_random_parameters, -//! create_random_proof, -//! prepare_verifying_key, -//! verify_proof -//! }; -//! use rand::{OsRng, Rand}; -//! -//! let rng = &mut OsRng::new(); -//! -//! // Create public parameters for our circuit -//! let params = { -//! let circuit = CubeRoot:: { -//! cube_root: None -//! }; -//! -//! generate_random_parameters::( -//! circuit, -//! rng -//! ).unwrap() -//! }; -//! -//! // Prepare the verifying key for verification -//! let pvk = prepare_verifying_key(¶ms.vk); -//! -//! // Let's start making proofs! -//! for _ in 0..50 { -//! // Verifier picks a cube in the field. -//! // Let's just make a random one. -//! let root = Fr::rand(rng); -//! let mut cube = root; -//! cube.square(); -//! cube.mul_assign(&root); -//! -//! // Prover gets the cube, figures out the cube -//! // root, and makes the proof: -//! let proof = create_random_proof( -//! CubeRoot:: { -//! cube_root: Some(root) -//! }, ¶ms, rng -//! ).unwrap(); -//! -//! // Verifier checks the proof against the cube -//! assert!(verify_proof(&pvk, &proof, &[cube]).unwrap()); -//! } -//! ``` -//! ## Creating parameters -//! -//! Notice in the previous example that we created our zk-SNARK -//! parameters by calling `generate_random_parameters`. However, -//! if you wanted you could have called `generate_parameters` -//! with some secret numbers you chose, and kept them for -//! yourself. Given those numbers, you can create false proofs. -//! -//! In order to convince others you didn't, a multi-party -//! computation (MPC) can be used. The MPC has the property that -//! only one participant needs to be honest for the parameters to -//! be secure. This crate (`phase2`) is about creating parameters -//! securely using such an MPC. -//! -//! Let's start by using `phase2` to create some base parameters -//! for our circuit: -//! -//! ```rust,ignore -//! extern crate phase2; -//! -//! let mut params = phase2::MPCParameters::new(CubeRoot { -//! cube_root: None -//! }).unwrap(); -//! ``` -//! -//! The first time you try this, it will try to read a file like -//! `phase1radix2m2` from the current directory. You need to grab -//! that from the [Powers of Tau](https://lists.z.cash.foundation/pipermail/zapps-wg/2018/000362.html). -//! -//! These parameters are not safe to use; false proofs can be -//! created for them. Let's contribute some randomness to these -//! parameters. -//! -//! ```rust,ignore -//! // Contribute randomness to the parameters. Remember this hash, -//! // it's how we know our contribution is in the parameters! -//! let hash = params.contribute(rng); -//! ``` -//! -//! These parameters are now secure to use, so long as you weren't -//! malicious. That may not be convincing to others, so let them -//! contribute randomness too! `params` can be serialized and sent -//! elsewhere, where they can do the same thing and send new -//! parameters back to you. Only one person needs to be honest for -//! the final parameters to be secure. -//! -//! Once you're done setting up the parameters, you can verify the -//! parameters: -//! -//! ```rust,ignore -//! let contributions = params.verify(CubeRoot { -//! cube_root: None -//! }).expect("parameters should be valid!"); -//! -//! // We need to check the `contributions` to see if our `hash` -//! // is in it (see above, when we first contributed) -//! assert!(phase2::contains_contribution(&contributions, &hash)); -//! ``` -//! -//! Great, now if you're happy, grab the Groth16 `Parameters` with -//! `params.params()`, so that you can interact with the bellman APIs -//! just as before. - -extern crate bellman; -extern crate byteorder; -extern crate pairing; -extern crate rand; -extern crate rand_chacha; - -use rayon::prelude::*; - -use blake2::{Blake2b512, Digest}; - -use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; - -use std::{ - fmt, - fs::File, - io::{self, BufReader, Error, ErrorKind, Read, Write}, - ops::{AddAssign, Mul}, - sync::Arc, -}; - -use ff::{Field, PrimeField}; - -use pairing::PairingCurveAffine; - -use group::{Curve, Group, Wnaf}; - -use rand_chacha::ChaChaRng; - -use bellman::{ - groth16::{Parameters, VerifyingKey}, - Circuit, ConstraintSystem, Index, LinearCombination, SynthesisError, Variable, -}; - -use bls12_381::{Bls12, G1Affine, G1Projective, G2Affine, G2Projective}; - -use rand::{Rng, SeedableRng}; - -/// This is our assembly structure that we'll use to synthesize the -/// circuit into a QAP. -struct KeypairAssembly { - num_inputs: usize, - num_aux: usize, - num_constraints: usize, - at_inputs: Vec>, - bt_inputs: Vec>, - ct_inputs: Vec>, - at_aux: Vec>, - bt_aux: Vec>, - ct_aux: Vec>, -} - -impl ConstraintSystem for KeypairAssembly { - type Root = Self; - - fn alloc(&mut self, _: A, _: F) -> Result - where - F: FnOnce() -> Result, - A: FnOnce() -> AR, - AR: Into, - { - // There is no assignment, so we don't even invoke the - // function for obtaining one. - - let index = self.num_aux; - self.num_aux += 1; - - self.at_aux.push(vec![]); - self.bt_aux.push(vec![]); - self.ct_aux.push(vec![]); - - Ok(Variable::new_unchecked(Index::Aux(index))) - } - - fn alloc_input(&mut self, _: A, _: F) -> Result - where - F: FnOnce() -> Result, - A: FnOnce() -> AR, - AR: Into, - { - // There is no assignment, so we don't even invoke the - // function for obtaining one. - - let index = self.num_inputs; - self.num_inputs += 1; - - self.at_inputs.push(vec![]); - self.bt_inputs.push(vec![]); - self.ct_inputs.push(vec![]); - - Ok(Variable::new_unchecked(Index::Input(index))) - } - - fn enforce(&mut self, _: A, a: LA, b: LB, c: LC) - where - A: FnOnce() -> AR, - AR: Into, - LA: FnOnce(LinearCombination) -> LinearCombination, - LB: FnOnce(LinearCombination) -> LinearCombination, - LC: FnOnce(LinearCombination) -> LinearCombination, - { - fn eval( - l: LinearCombination, - inputs: &mut [Vec<(Scalar, usize)>], - aux: &mut [Vec<(Scalar, usize)>], - this_constraint: usize, - ) { - for &(var, coeff) in l.as_ref() { - match var.get_unchecked() { - Index::Input(id) => inputs[id].push((coeff, this_constraint)), - Index::Aux(id) => aux[id].push((coeff, this_constraint)), - } - } - } - - eval( - a(LinearCombination::zero()), - &mut self.at_inputs, - &mut self.at_aux, - self.num_constraints, - ); - eval( - b(LinearCombination::zero()), - &mut self.bt_inputs, - &mut self.bt_aux, - self.num_constraints, - ); - eval( - c(LinearCombination::zero()), - &mut self.ct_inputs, - &mut self.ct_aux, - self.num_constraints, - ); - - self.num_constraints += 1; - } - - fn push_namespace(&mut self, _: N) - where - NR: Into, - N: FnOnce() -> NR, - { - // Do nothing; we don't care about namespaces in this context. - } - - fn pop_namespace(&mut self) { - // Do nothing; we don't care about namespaces in this context. - } - - fn get_root(&mut self) -> &mut Self::Root { - self - } -} - -#[derive(Debug)] -pub struct FailedVerification; - -impl fmt::Display for FailedVerification { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Verification failed") - } -} - -/// MPC parameters are just like bellman `Parameters` except, when serialized, -/// they contain a transcript of contributions at the end, which can be verified. -#[derive(Clone)] -pub struct MPCParameters { - params: Parameters, - cs_hash: [u8; 64], - contributions: Vec, -} - -impl PartialEq for MPCParameters { - fn eq(&self, other: &MPCParameters) -> bool { - self.params == other.params - && self.cs_hash[..] == other.cs_hash[..] - && self.contributions == other.contributions - } -} - -impl MPCParameters { - /// Create new Groth16 parameters (compatible with bellman) for a - /// given circuit. The resulting parameters are unsafe to use - /// until there are contributions (see `contribute()`). - pub fn new(circuit: C) -> Result - where - C: Circuit, - { - let mut assembly: KeypairAssembly = KeypairAssembly { - num_inputs: 0, - num_aux: 0, - num_constraints: 0, - at_inputs: vec![], - bt_inputs: vec![], - ct_inputs: vec![], - at_aux: vec![], - bt_aux: vec![], - ct_aux: vec![], - }; - - // Allocate the "one" input variable - assembly.alloc_input(|| "", || Ok(bls12_381::Scalar::one()))?; - - // Synthesize the circuit. - circuit.synthesize(&mut assembly)?; - - // Input constraints to ensure full density of IC query - // x * 0 = 0 - for i in 0..assembly.num_inputs { - assembly.enforce( - || "", - |lc| lc + Variable::new_unchecked(Index::Input(i)), - |lc| lc, - |lc| lc, - ); - } - - // Compute the size of our evaluation domain - let mut m = 1; - let mut exp = 0; - while m < assembly.num_constraints { - m *= 2; - exp += 1; - - // Powers of Tau ceremony can't support more than 2^21 - if exp > 21 { - return Err(SynthesisError::PolynomialDegreeTooLarge); - } - } - - // Try to load "phase1radix2m{}" - let f = match File::open(format!("phase1radix2m{}", exp)) { - Ok(f) => f, - Err(e) => { - panic!("Couldn't load phase1radix2m{}: {:?}", exp, e); - } - }; - let f = &mut BufReader::with_capacity(1024 * 1024, f); - - let read_g1 = |reader: &mut BufReader| -> io::Result { - let mut byte_buffer: [u8; 96] = [0u8; 96]; - reader.read_exact(byte_buffer.as_mut())?; - - let point = bls12_381::G1Affine::from_uncompressed(&byte_buffer) - .unwrap_or_else(G1Affine::identity); - - if bool::from(point.is_identity()) { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "point at infinity", - )); - } - - Ok(point) - }; - - let read_g2 = |reader: &mut BufReader| -> io::Result { - let mut byte_buffer: [u8; 192] = [0u8; 192]; - reader.read_exact(byte_buffer.as_mut())?; - - let point = bls12_381::G2Affine::from_uncompressed(&byte_buffer) - .unwrap_or_else(G2Affine::identity); - - if bool::from(point.is_identity()) { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "point at infinity", - )); - } - - Ok(point) - }; - - let alpha = read_g1(f)?; - let beta_g1 = read_g1(f)?; - let beta_g2 = read_g2(f)?; - - let mut coeffs_g1 = Vec::with_capacity(m); - for _ in 0..m { - coeffs_g1.push(read_g1(f)?); - } - - let mut coeffs_g2 = Vec::with_capacity(m); - for _ in 0..m { - coeffs_g2.push(read_g2(f)?); - } - - let mut alpha_coeffs_g1 = Vec::with_capacity(m); - for _ in 0..m { - alpha_coeffs_g1.push(read_g1(f)?); - } - - let mut beta_coeffs_g1 = Vec::with_capacity(m); - for _ in 0..m { - beta_coeffs_g1.push(read_g1(f)?); - } - - let mut h = Vec::with_capacity(m - 1); - for _ in 0..(m - 1) { - h.push(read_g1(f)?); - } - - // TODO: Decide whether we should do computations on G1Projective of G1Affine (one is probably faster) - let mut ic = vec![G1Projective::identity(); assembly.num_inputs]; - let mut l = vec![G1Projective::identity(); assembly.num_aux]; - let mut a_g1 = vec![G1Projective::identity(); assembly.num_inputs + assembly.num_aux]; - let mut b_g1 = vec![G1Projective::identity(); assembly.num_inputs + assembly.num_aux]; - let mut b_g2 = vec![G2Projective::identity(); assembly.num_inputs + assembly.num_aux]; - - #[allow(clippy::too_many_arguments)] - fn eval( - // Lagrange coefficients for tau - coeffs_g1: &[G1Affine], - coeffs_g2: &[G2Affine], - alpha_coeffs_g1: &[G1Affine], - beta_coeffs_g1: &[G1Affine], - - // QAP polynomials - at: &[Vec<(bls12_381::Scalar, usize)>], - bt: &[Vec<(bls12_381::Scalar, usize)>], - ct: &[Vec<(bls12_381::Scalar, usize)>], - - // Resulting evaluated QAP polynomials - a_g1: &mut [G1Projective], - b_g1: &mut [G1Projective], - b_g2: &mut [G2Projective], - ext: &mut [G1Projective], - ) { - // Sanity check - assert_eq!(a_g1.len(), at.len()); - assert_eq!(a_g1.len(), bt.len()); - assert_eq!(a_g1.len(), ct.len()); - assert_eq!(a_g1.len(), b_g1.len()); - assert_eq!(a_g1.len(), b_g2.len()); - assert_eq!(a_g1.len(), ext.len()); - - (at, a_g1).into_par_iter().for_each(|(at, a_g1)| { - let ag1_coeffs = at.par_iter().map(|&(coeff, lag)| coeffs_g1[lag].mul(coeff)); - let agc_result: G1Projective = ag1_coeffs.sum(); - a_g1.add_assign(&agc_result); - }); - - (bt, b_g1, b_g2) - .into_par_iter() - .for_each(|(bt, b_g1, b_g2)| { - // b_g1 - let bg1_coeffs = bt.par_iter().map(|&(coeff, lag)| coeffs_g1[lag].mul(coeff)); - let bg1_result: G1Projective = bg1_coeffs.sum(); - b_g1.add_assign(&bg1_result); - - // b_g2 - let bg2_coeffs = bt.par_iter().map(|&(coeff, lag)| coeffs_g2[lag].mul(coeff)); - let bg2_result: G2Projective = bg2_coeffs.sum(); - b_g2.add_assign(&bg2_result); - }); - - (at, bt, ct, ext) - .into_par_iter() - .for_each(|(at, bt, ct, ext)| { - let ext_at = at - .par_iter() - .map(|&(coeff, lag)| beta_coeffs_g1[lag].mul(coeff)); - let ext_bt = bt - .par_iter() - .map(|&(coeff, lag)| alpha_coeffs_g1[lag].mul(coeff)); - let ext_ct = ct.par_iter().map(|&(coeff, lag)| coeffs_g1[lag].mul(coeff)); - let ext_chained: G1Projective = ext_at.chain(ext_bt).chain(ext_ct).sum(); - ext.add_assign(ext_chained); - }); - } - - // Evaluate for inputs. - eval( - &coeffs_g1, - &coeffs_g2, - &alpha_coeffs_g1, - &beta_coeffs_g1, - &assembly.at_inputs, - &assembly.bt_inputs, - &assembly.ct_inputs, - &mut a_g1[0..assembly.num_inputs], - &mut b_g1[0..assembly.num_inputs], - &mut b_g2[0..assembly.num_inputs], - &mut ic, - ); - - // Evaluate for auxiliary variables. - eval( - &coeffs_g1, - &coeffs_g2, - &alpha_coeffs_g1, - &beta_coeffs_g1, - &assembly.at_aux, - &assembly.bt_aux, - &assembly.ct_aux, - &mut a_g1[assembly.num_inputs..], - &mut b_g1[assembly.num_inputs..], - &mut b_g2[assembly.num_inputs..], - &mut l, - ); - - // Don't allow any elements be unconstrained, so that - // the L query is always fully dense. - for e in l.iter() { - if bool::from(e.is_identity()) { - return Err(SynthesisError::UnconstrainedVariable); - } - } - - let mut ic_affine = vec![G1Affine::identity(); assembly.num_inputs]; - G1Projective::batch_normalize(&ic[..], &mut ic_affine[..]); - - let mut l_affine = vec![G1Affine::identity(); assembly.num_aux]; - G1Projective::batch_normalize(&l[..], &mut l_affine[..]); - - let mut a_g1_affine = vec![G1Affine::identity(); assembly.num_inputs + assembly.num_aux]; - G1Projective::batch_normalize(&a_g1[..], &mut a_g1_affine[..]); - - let mut b_g1_affine = vec![G1Affine::identity(); assembly.num_inputs + assembly.num_aux]; - G1Projective::batch_normalize(&b_g1[..], &mut b_g1_affine[..]); - - let mut b_g2_affine = vec![G2Affine::identity(); assembly.num_inputs + assembly.num_aux]; - G2Projective::batch_normalize(&b_g2[..], &mut b_g2_affine[..]); - - let vk = VerifyingKey { - alpha_g1: alpha, - beta_g1, - beta_g2, - gamma_g2: G2Affine::generator(), - delta_g1: G1Affine::generator(), - delta_g2: G2Affine::generator(), - ic: ic_affine, - }; - - let params = Parameters { - vk, - h: Arc::new(h), - l: Arc::new(l_affine), - - // Filter points at infinity away from A/B queries - a: Arc::new( - a_g1_affine - .into_iter() - .filter(|e| !bool::from(e.is_identity())) - .collect(), - ), - b_g1: Arc::new( - b_g1_affine - .into_iter() - .filter(|e| !bool::from(e.is_identity())) - .collect(), - ), - b_g2: Arc::new( - b_g2_affine - .into_iter() - .filter(|e| !bool::from(e.is_identity())) - .collect(), - ), - }; - - let h = { - let sink = io::sink(); - let mut sink = HashWriter::new(sink); - - params.write(&mut sink).unwrap(); - - sink.into_hash() - }; - - let mut cs_hash = [0; 64]; - cs_hash.copy_from_slice(h.as_ref()); - - Ok(MPCParameters { - params, - cs_hash, - contributions: vec![], - }) - } - - /// Get the underlying Groth16 `Parameters` - pub fn get_params(&self) -> &Parameters { - &self.params - } - - /// Contributes some randomness to the parameters. Only one - /// contributor needs to be honest for the parameters to be - /// secure. - /// - /// This function returns a "hash" that is bound to the - /// contribution. Contributors can use this hash to make - /// sure their contribution is in the final parameters, by - /// checking to see if it appears in the output of - /// `MPCParameters::verify`. - pub fn contribute(&mut self, rng: &mut R) -> [u8; 64] { - // Generate a keypair - let (pubkey, privkey) = keypair(rng, self); - - fn batch_exp(bases: &mut [G1Affine], coeff: bls12_381::Scalar) { - bases.par_iter_mut().for_each(|base| { - let mut wnaf = Wnaf::new(); - - *base = G1Affine::from(wnaf.base(G1Projective::from(*base), 1).scalar(&coeff)); - }); - } - - let delta_inv = privkey.delta.invert().unwrap(); - let mut l = (self.params.l[..]).to_vec(); - let mut h = (self.params.h[..]).to_vec(); - batch_exp(&mut l, delta_inv); - batch_exp(&mut h, delta_inv); - self.params.l = Arc::new(l); - self.params.h = Arc::new(h); - - self.params.vk.delta_g1 = self.params.vk.delta_g1.mul(privkey.delta).to_affine(); - self.params.vk.delta_g2 = self.params.vk.delta_g2.mul(privkey.delta).to_affine(); - - self.contributions.push(pubkey.clone()); - - // Calculate the hash of the public key and return it - { - let sink = io::sink(); - let mut sink = HashWriter::new(sink); - pubkey.write(&mut sink).unwrap(); - let h = sink.into_hash(); - let mut response = [0u8; 64]; - response.copy_from_slice(h.as_ref()); - response - } - } - - /// Verify the correctness of the parameters, given a circuit - /// instance. This will return all of the hashes that - /// contributors obtained when they ran - /// `MPCParameters::contribute`, for ensuring that contributions - /// exist in the final parameters. - pub fn verify>( - &self, - circuit: C, - ) -> Result, FailedVerification> { - let initial_params = MPCParameters::new(circuit).map_err(|_| FailedVerification)?; - - // H/L will change, but should have same length - if initial_params.params.h.len() != self.params.h.len() { - return Err(FailedVerification); - } - if initial_params.params.l.len() != self.params.l.len() { - return Err(FailedVerification); - } - - // A/B_G1/B_G2 doesn't change at all - if initial_params.params.a != self.params.a { - return Err(FailedVerification); - } - if initial_params.params.b_g1 != self.params.b_g1 { - return Err(FailedVerification); - } - if initial_params.params.b_g2 != self.params.b_g2 { - return Err(FailedVerification); - } - - // alpha/beta/gamma don't change - if initial_params.params.vk.alpha_g1 != self.params.vk.alpha_g1 { - return Err(FailedVerification); - } - if initial_params.params.vk.beta_g1 != self.params.vk.beta_g1 { - return Err(FailedVerification); - } - if initial_params.params.vk.beta_g2 != self.params.vk.beta_g2 { - return Err(FailedVerification); - } - if initial_params.params.vk.gamma_g2 != self.params.vk.gamma_g2 { - return Err(FailedVerification); - } - - // IC shouldn't change, as gamma doesn't change - if initial_params.params.vk.ic != self.params.vk.ic { - return Err(FailedVerification); - } - - // cs_hash should be the same - if initial_params.cs_hash[..] != self.cs_hash[..] { - return Err(FailedVerification); - } - - let sink = io::sink(); - let mut sink = HashWriter::new(sink); - sink.write_all(&initial_params.cs_hash[..]).unwrap(); - - let mut current_delta = G1Affine::generator(); - let mut result = vec![]; - - for pubkey in &self.contributions { - let mut our_sink = sink.clone(); - our_sink - .write_all(pubkey.s.to_uncompressed().as_ref()) - .unwrap(); - our_sink - .write_all(pubkey.s_delta.to_uncompressed().as_ref()) - .unwrap(); - - pubkey.write(&mut sink).unwrap(); - - let h = our_sink.into_hash(); - - // The transcript must be consistent - if &pubkey.transcript[..] != h.as_ref() { - return Err(FailedVerification); - } - - let r = hash_to_g2(h.as_ref()); - - // Check the signature of knowledge - if !same_ratio((r, pubkey.r_delta), (pubkey.s, pubkey.s_delta)) { - return Err(FailedVerification); - } - - // Check the change from the old delta is consistent - if !same_ratio((current_delta, pubkey.delta_after), (r, pubkey.r_delta)) { - return Err(FailedVerification); - } - - current_delta = pubkey.delta_after; - - { - let sink = io::sink(); - let mut sink = HashWriter::new(sink); - pubkey.write(&mut sink).unwrap(); - let h = sink.into_hash(); - let mut response = [0u8; 64]; - response.copy_from_slice(h.as_ref()); - result.push(response); - } - } - - // Current parameters should have consistent delta in G1 - if current_delta != self.params.vk.delta_g1 { - return Err(FailedVerification); - } - - // Current parameters should have consistent delta in G2 - if !same_ratio( - (G1Affine::generator(), current_delta), - (G2Affine::generator(), self.params.vk.delta_g2), - ) { - return Err(FailedVerification); - } - - // H and L queries should be updated with delta^-1 - if !same_ratio( - merge_pairs(&initial_params.params.h, &self.params.h), - (self.params.vk.delta_g2, G2Affine::generator()), // reversed for inverse - ) { - return Err(FailedVerification); - } - - if !same_ratio( - merge_pairs(&initial_params.params.l, &self.params.l), - (self.params.vk.delta_g2, G2Affine::generator()), // reversed for inverse - ) { - return Err(FailedVerification); - } - - Ok(result) - } - - /// Serialize these parameters. The serialized parameters - /// can be read by bellman as Groth16 `Parameters`. - pub fn write(&self, mut writer: W) -> io::Result<()> { - self.params.write(&mut writer)?; - writer.write_all(&self.cs_hash)?; - - writer.write_u32::(self.contributions.len() as u32)?; - for pubkey in &self.contributions { - pubkey.write(&mut writer)?; - } - - Ok(()) - } - - /// Deserialize these parameters. If `checked` is false, - /// we won't perform curve validity and group order - /// checks. - pub fn read(mut reader: R, checked: bool) -> io::Result { - // Parameters - let read_g1 = |reader: &mut R| -> io::Result<[u8; 96]> { - let mut repr: [u8; 96] = [0u8; 96]; - reader.read_exact(repr.as_mut())?; - Ok(repr) - }; - - let process_g1 = |repr: &[u8; 96]| -> io::Result { - let affine = if checked { - bls12_381::G1Affine::from_uncompressed(repr) - } else { - bls12_381::G1Affine::from_uncompressed_unchecked(repr) - }; - - let affine = if affine.is_some().into() { - Ok(affine.unwrap()) - } else { - Err(io::Error::new(io::ErrorKind::InvalidData, "invalid G1")) - }; - - affine.and_then(|e| { - if e.is_identity().into() { - Err(io::Error::new( - io::ErrorKind::InvalidData, - "point at infinity", - )) - } else { - Ok(e) - } - }) - }; - - let read_g2 = |reader: &mut R| -> io::Result<[u8; 192]> { - let mut repr: [u8; 192] = [0u8; 192]; - reader.read_exact(repr.as_mut())?; - Ok(repr) - }; - - let process_g2 = |repr: &[u8; 192]| -> io::Result { - let affine = if checked { - G2Affine::from_uncompressed(repr) - } else { - G2Affine::from_uncompressed_unchecked(repr) - }; - - let affine = if affine.is_some().into() { - Ok(affine.unwrap()) - } else { - Err(io::Error::new(io::ErrorKind::InvalidData, "invalid G2")) - }; - - affine.and_then(|e| { - if e.is_identity().into() { - Err(io::Error::new( - io::ErrorKind::InvalidData, - "point at infinity", - )) - } else { - Ok(e) - } - }) - }; - - let vk = VerifyingKey::read(&mut reader)?; - - let h = { - let len = reader.read_u32::()? as usize; - let mut bufs = Vec::with_capacity(len); - - for _ in 0..len { - bufs.push(read_g1(&mut reader)?); - } - - let h: Result<_, _> = bufs.par_iter().map(process_g1).collect(); - h - }?; - - let l = { - let len = reader.read_u32::()? as usize; - let mut bufs = Vec::with_capacity(len); - - for _ in 0..len { - bufs.push(read_g1(&mut reader)?); - } - - let l: Result<_, _> = bufs.par_iter().map(process_g1).collect(); - l - }?; - - let a = { - let len = reader.read_u32::()? as usize; - let mut bufs = Vec::with_capacity(len); - - for _ in 0..len { - bufs.push(read_g1(&mut reader)?); - } - - let a: Result<_, _> = bufs.par_iter().map(process_g1).collect(); - a - }?; - - let b_g1 = { - let len = reader.read_u32::()? as usize; - let mut bufs = Vec::with_capacity(len); - - for _ in 0..len { - bufs.push(read_g1(&mut reader)?); - } - - let b_g1: Result<_, _> = bufs.par_iter().map(process_g1).collect(); - b_g1 - }?; - - let b_g2 = { - let len = reader.read_u32::()? as usize; - let mut bufs = Vec::with_capacity(len); - - for _ in 0..len { - bufs.push(read_g2(&mut reader)?); - } - - let b_g2: Result<_, _> = bufs.par_iter().map(process_g2).collect(); - b_g2 - }?; - - let params = Parameters { - vk, - h: Arc::new(h), - l: Arc::new(l), - a: Arc::new(a), - b_g1: Arc::new(b_g1), - b_g2: Arc::new(b_g2), - }; - - // Contributions - let mut cs_hash = [0u8; 64]; - reader.read_exact(&mut cs_hash)?; - - let contributions_len = reader.read_u32::()? as usize; - - let mut contributions = vec![]; - for _ in 0..contributions_len { - contributions.push(PublicKey::read(&mut reader)?); - } - - Ok(MPCParameters { - params, - cs_hash, - contributions, - }) - } -} - -/// This allows others to verify that you contributed. The hash produced -/// by `MPCParameters::contribute` is just a BLAKE2b hash of this object. -#[derive(Clone)] -struct PublicKey { - /// This is the delta (in G1) after the transformation, kept so that we - /// can check correctness of the public keys without having the entire - /// interstitial parameters for each contribution. - delta_after: G1Affine, - - /// Random element chosen by the contributor. - s: G1Affine, - - /// That element, taken to the contributor's secret delta. - s_delta: G1Affine, - - /// r is H(last_pubkey | s | s_delta), r_delta proves knowledge of delta - r_delta: G2Affine, - - /// Hash of the transcript (used for mapping to r) - transcript: [u8; 64], -} - -impl PublicKey { - fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_all(self.delta_after.to_uncompressed().as_ref())?; - writer.write_all(self.s.to_uncompressed().as_ref())?; - writer.write_all(self.s_delta.to_uncompressed().as_ref())?; - writer.write_all(self.r_delta.to_uncompressed().as_ref())?; - writer.write_all(&self.transcript)?; - - Ok(()) - } - - fn read(mut reader: R) -> io::Result { - let mut g1_repr: [u8; 96] = [0u8; 96]; - let mut g2_repr: [u8; 192] = [0u8; 192]; - - reader.read_exact(g1_repr.as_mut())?; - let delta_after = G1Affine::from_uncompressed(&g1_repr).unwrap_or_else(G1Affine::identity); - - if bool::from(delta_after.is_identity()) { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "point at infinity", - )); - } - - reader.read_exact(g1_repr.as_mut())?; - let s = G1Affine::from_uncompressed(&g1_repr).unwrap_or_else(G1Affine::identity); - - if bool::from(s.is_identity()) { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "point at infinity", - )); - } - - reader.read_exact(g1_repr.as_mut())?; - let s_delta = G1Affine::from_uncompressed(&g1_repr).unwrap_or_else(G1Affine::identity); - - if bool::from(s_delta.is_identity()) { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "point at infinity", - )); - } - - reader.read_exact(g2_repr.as_mut())?; - let r_delta = G2Affine::from_uncompressed(&g2_repr).unwrap_or_else(G2Affine::identity); - - if bool::from(r_delta.is_identity()) { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "point at infinity", - )); - } - - let mut transcript = [0u8; 64]; - reader.read_exact(&mut transcript)?; - - Ok(PublicKey { - delta_after, - s, - s_delta, - r_delta, - transcript, - }) - } -} - -impl PartialEq for PublicKey { - fn eq(&self, other: &PublicKey) -> bool { - self.delta_after == other.delta_after - && self.s == other.s - && self.s_delta == other.s_delta - && self.r_delta == other.r_delta - && self.transcript[..] == other.transcript[..] - } -} - -fn failed_contribution_error() -> std::io::Error { - Error::new(ErrorKind::Other, "Failed to verify contribution") -} - -/// Verify a contribution, given the old parameters and -/// the new parameters. Returns the hash of the contribution. -pub fn verify_contribution( - before: &MPCParameters, - after: &MPCParameters, -) -> Result<[u8; 64], std::io::Error> { - // Transformation involves a single new object - if after.contributions.len() != (before.contributions.len() + 1) { - return Err(failed_contribution_error()); - } - - // None of the previous transformations should change - if before.contributions[..] != after.contributions[0..before.contributions.len()] { - return Err(failed_contribution_error()); - } - - // H/L will change, but should have same length - if before.params.h.len() != after.params.h.len() { - return Err(failed_contribution_error()); - } - if before.params.l.len() != after.params.l.len() { - return Err(failed_contribution_error()); - } - - // A/B_G1/B_G2 doesn't change at all - if before.params.a != after.params.a { - return Err(failed_contribution_error()); - } - if before.params.b_g1 != after.params.b_g1 { - return Err(failed_contribution_error()); - } - if before.params.b_g2 != after.params.b_g2 { - return Err(failed_contribution_error()); - } - - // alpha/beta/gamma don't change - if before.params.vk.alpha_g1 != after.params.vk.alpha_g1 { - return Err(failed_contribution_error()); - } - if before.params.vk.beta_g1 != after.params.vk.beta_g1 { - return Err(failed_contribution_error()); - } - if before.params.vk.beta_g2 != after.params.vk.beta_g2 { - return Err(failed_contribution_error()); - } - if before.params.vk.gamma_g2 != after.params.vk.gamma_g2 { - return Err(failed_contribution_error()); - } - - // IC shouldn't change, as gamma doesn't change - if before.params.vk.ic != after.params.vk.ic { - return Err(failed_contribution_error()); - } - - // cs_hash should be the same - if before.cs_hash[..] != after.cs_hash[..] { - return Err(failed_contribution_error()); - } - - let sink = io::sink(); - let mut sink = HashWriter::new(sink); - sink.write_all(&before.cs_hash[..])?; - - for pubkey in &before.contributions { - pubkey.write(&mut sink)?; - } - - let pubkey = after - .contributions - .last() - .ok_or_else(failed_contribution_error)?; - sink.write_all(pubkey.s.to_uncompressed().as_ref())?; - sink.write_all(pubkey.s_delta.to_uncompressed().as_ref())?; - - let h = sink.into_hash(); - - // The transcript must be consistent - if &pubkey.transcript[..] != h.as_ref() { - return Err(failed_contribution_error()); - } - - let r = hash_to_g2(h.as_ref()); - - // Check the signature of knowledge - if !same_ratio((r, pubkey.r_delta), (pubkey.s, pubkey.s_delta)) { - return Err(failed_contribution_error()); - } - - // Check the change from the old delta is consistent - if !same_ratio( - (before.params.vk.delta_g1, pubkey.delta_after), - (r, pubkey.r_delta), - ) { - return Err(failed_contribution_error()); - } - - // Current parameters should have consistent delta in G1 - if pubkey.delta_after != after.params.vk.delta_g1 { - return Err(failed_contribution_error()); - } - - // Current parameters should have consistent delta in G2 - if !same_ratio( - (G1Affine::generator(), pubkey.delta_after), - (G2Affine::generator(), after.params.vk.delta_g2), - ) { - return Err(failed_contribution_error()); - } - - // H and L queries should be updated with delta^-1 - if !same_ratio( - merge_pairs(&before.params.h, &after.params.h), - (after.params.vk.delta_g2, before.params.vk.delta_g2), // reversed for inverse - ) { - return Err(failed_contribution_error()); - } - - if !same_ratio( - merge_pairs(&before.params.l, &after.params.l), - (after.params.vk.delta_g2, before.params.vk.delta_g2), // reversed for inverse - ) { - return Err(failed_contribution_error()); - } - - let sink = io::sink(); - let mut sink = HashWriter::new(sink); - pubkey.write(&mut sink)?; - let h = sink.into_hash(); - let mut response = [0u8; 64]; - response.copy_from_slice(h.as_ref()); - - Ok(response) -} - -/// Checks if pairs have the same ratio. -fn same_ratio(g1: (G1, G1), g2: (G1::Pair, G1::Pair)) -> bool { - g1.0.pairing_with(&g2.1) == g1.1.pairing_with(&g2.0) -} - -/// Computes a random linear combination over v1/v2. -/// -/// Checking that many pairs of elements are exponentiated by -/// the same `x` can be achieved (with high probability) with -/// the following technique: -/// -/// Given v1 = [a, b, c] and v2 = [as, bs, cs], compute -/// (a*r1 + b*r2 + c*r3, (as)*r1 + (bs)*r2 + (cs)*r3) for some -/// random r1, r2, r3. Given (g, g^s)... -/// -/// e(g, (as)*r1 + (bs)*r2 + (cs)*r3) = e(g^s, a*r1 + b*r2 + c*r3) -/// -/// ... with high probability. -fn merge_pairs(v1: &[G1Affine], v2: &[G1Affine]) -> (G1Affine, G1Affine) { - use rand::thread_rng; - - assert_eq!(v1.len(), v2.len()); - - let result = (v1, v2) - .into_par_iter() - .map(|(&v1, &v2)| { - // We do not need to be overly cautious of the RNG - // used for this check. - let rng = &mut thread_rng(); - let rho = bls12_381::Scalar::random(&mut *rng); - let mut new_wnaf = Wnaf::new(); - let mut wnaf = new_wnaf.scalar(&rho); - ( - wnaf.base(G1Projective::from(v1)), - wnaf.base(G1Projective::from(v2)), - ) - }) - .reduce( - || (G1Projective::identity(), G1Projective::identity()), - |a, b| (a.0 + b.0, a.1 + b.1), - ); - - (result.0.to_affine(), result.1.to_affine()) -} - -/// This needs to be destroyed by at least one participant -/// for the final parameters to be secure. -struct PrivateKey { - delta: bls12_381::Scalar, -} - -/// Compute a keypair, given the current parameters. Keypairs -/// cannot be reused for multiple contributions or contributions -/// in different parameters. -fn keypair(rng: &mut R, current: &MPCParameters) -> (PublicKey, PrivateKey) { - // Sample random delta - let delta: bls12_381::Scalar = bls12_381::Scalar::random(&mut *rng); - - // Compute delta s-pair in G1 - let s: G1Affine = G1Affine::from(G1Projective::random(rng)); - let s_delta = G1Affine::from(s.mul(delta)); - - // H(cs_hash | | s | s_delta) - let h = { - let sink = io::sink(); - let mut sink = HashWriter::new(sink); - - sink.write_all(¤t.cs_hash[..]).unwrap(); - for pubkey in ¤t.contributions { - pubkey.write(&mut sink).unwrap(); - } - sink.write_all(s.to_uncompressed().as_ref()).unwrap(); - sink.write_all(s_delta.to_uncompressed().as_ref()).unwrap(); - - sink.into_hash() - }; - - // This avoids making a weird assumption about the hash into the - // group. - let mut transcript = [0; 64]; - transcript.copy_from_slice(h.as_ref()); - - // Compute delta s-pair in G2 - let r = hash_to_g2(h.as_ref()); - let r_delta = G2Affine::from(r.mul(delta)); - - ( - PublicKey { - delta_after: G1Affine::from(current.params.vk.delta_g1.mul(delta)), - s, - s_delta, - r_delta, - transcript, - }, - PrivateKey { delta }, - ) -} - -/// Hashes to G2 using the first 32 bytes of `digest`. Panics if `digest` is less -/// than 32 bytes. -fn hash_to_g2(digest: &[u8]) -> G2Affine { - assert!(digest.len() >= 32); - - let digest_32: [u8; 32] = digest[..32].try_into().unwrap(); - - G2Affine::from(G2Projective::random(ChaChaRng::from_seed(digest_32))) -} - -/// Abstraction over a writer which hashes the data being written. -struct HashWriter { - writer: W, - hasher: Blake2b512, -} - -impl Clone for HashWriter { - fn clone(&self) -> HashWriter { - HashWriter { - writer: io::sink(), - hasher: self.hasher.clone(), - } - } -} - -impl HashWriter { - /// Construct a new `HashWriter` given an existing `writer` by value. - pub fn new(writer: W) -> Self { - HashWriter { - writer, - hasher: Blake2b512::new(), - } - } - - /// Destroy this writer and return the hash of what was written. - pub fn into_hash(self) -> [u8; 64] { - let mut tmp = [0u8; 64]; - tmp.copy_from_slice(self.hasher.finalize().as_ref()); - tmp - } -} - -impl Write for HashWriter { - fn write(&mut self, buf: &[u8]) -> io::Result { - let bytes = self.writer.write(buf)?; - - if bytes > 0 { - self.hasher.update(&buf[0..bytes]); - } - - Ok(bytes) - } - - fn flush(&mut self) -> io::Result<()> { - self.writer.flush() - } -} - -/// This is a cheap helper utility that exists purely -/// because Rust still doesn't have type-level integers -/// and so doesn't implement `PartialEq` for `[T; 64]` -pub fn contains_contribution(contributions: &[[u8; 64]], my_contribution: &[u8; 64]) -> bool { - for contrib in contributions { - if contrib[..] == my_contribution[..] { - return true; - } - } - - false -} diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index 3b5b81ba62..e60a175db8 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -3,8 +3,6 @@ /* auto-generated by NAPI-RS */ -export function contribute(inputPath: string, outputPath: string, seed?: string | undefined | null): Promise -export function verifyTransform(paramsPath: string, newParamsPath: string): Promise export const KEY_LENGTH: number export const NONCE_LENGTH: number export function randomBytes(bytesLength: number): Uint8Array diff --git a/ironfish-rust-nodejs/index.js b/ironfish-rust-nodejs/index.js index a858c6522d..949ef259cc 100644 --- a/ironfish-rust-nodejs/index.js +++ b/ironfish-rust-nodejs/index.js @@ -252,11 +252,9 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { FishHashContext, contribute, verifyTransform, KEY_LENGTH, NONCE_LENGTH, BoxKeyPair, randomBytes, boxMessage, unboxMessage, RollingFilter, initSignalHandler, triggerSegfault, ASSET_ID_LENGTH, ASSET_METADATA_LENGTH, ASSET_NAME_LENGTH, ASSET_LENGTH, Asset, NOTE_ENCRYPTION_KEY_LENGTH, MAC_LENGTH, ENCRYPTED_NOTE_PLAINTEXT_LENGTH, ENCRYPTED_NOTE_LENGTH, NoteEncrypted, PUBLIC_ADDRESS_LENGTH, RANDOMNESS_LENGTH, MEMO_LENGTH, AMOUNT_VALUE_LENGTH, DECRYPTED_NOTE_LENGTH, Note, PROOF_LENGTH, TRANSACTION_SIGNATURE_LENGTH, TRANSACTION_PUBLIC_KEY_RANDOMNESS_LENGTH, TRANSACTION_EXPIRATION_LENGTH, TRANSACTION_FEE_LENGTH, LATEST_TRANSACTION_VERSION, TransactionPosted, Transaction, verifyTransactions, UnsignedTransaction, LanguageCode, generateKey, spendingKeyToWords, wordsToSpendingKey, generateKeyFromPrivateKey, initializeSapling, FoundBlockResult, ThreadPoolHandler, isValidPublicAddress, multisig } = nativeBinding +const { FishHashContext, KEY_LENGTH, NONCE_LENGTH, BoxKeyPair, randomBytes, boxMessage, unboxMessage, RollingFilter, initSignalHandler, triggerSegfault, ASSET_ID_LENGTH, ASSET_METADATA_LENGTH, ASSET_NAME_LENGTH, ASSET_LENGTH, Asset, NOTE_ENCRYPTION_KEY_LENGTH, MAC_LENGTH, ENCRYPTED_NOTE_PLAINTEXT_LENGTH, ENCRYPTED_NOTE_LENGTH, NoteEncrypted, PUBLIC_ADDRESS_LENGTH, RANDOMNESS_LENGTH, MEMO_LENGTH, AMOUNT_VALUE_LENGTH, DECRYPTED_NOTE_LENGTH, Note, PROOF_LENGTH, TRANSACTION_SIGNATURE_LENGTH, TRANSACTION_PUBLIC_KEY_RANDOMNESS_LENGTH, TRANSACTION_EXPIRATION_LENGTH, TRANSACTION_FEE_LENGTH, LATEST_TRANSACTION_VERSION, TransactionPosted, Transaction, verifyTransactions, UnsignedTransaction, LanguageCode, generateKey, spendingKeyToWords, wordsToSpendingKey, generateKeyFromPrivateKey, initializeSapling, FoundBlockResult, ThreadPoolHandler, isValidPublicAddress, multisig } = nativeBinding module.exports.FishHashContext = FishHashContext -module.exports.contribute = contribute -module.exports.verifyTransform = verifyTransform module.exports.KEY_LENGTH = KEY_LENGTH module.exports.NONCE_LENGTH = NONCE_LENGTH module.exports.BoxKeyPair = BoxKeyPair diff --git a/supply-chain/config.toml b/supply-chain/config.toml index 24fb678aa7..1d70d75448 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -22,9 +22,6 @@ url = "https://raw.githubusercontent.com/mozilla/supply-chain/main/audits.toml" [imports.zcash] url = "https://raw.githubusercontent.com/zcash/rust-ecosystem/main/supply-chain/audits.toml" -[policy.bellman] -audit-as-crates-io = true - [policy.bellperson] audit-as-crates-io = true @@ -89,10 +86,6 @@ criteria = "safe-to-deploy" version = "0.8.1" criteria = "safe-to-deploy" -[[exemptions.bellman]] -version = "0.13.1" -criteria = "safe-to-deploy" - [[exemptions.bellperson]] version = "0.24.1" criteria = "safe-to-deploy" @@ -113,10 +106,6 @@ criteria = "safe-to-deploy" version = "1.0.1" criteria = "safe-to-deploy" -[[exemptions.blake2]] -version = "0.10.6" -criteria = "safe-to-deploy" - [[exemptions.blake2b_simd]] version = "1.0.0" criteria = "safe-to-deploy" @@ -397,14 +386,6 @@ criteria = "safe-to-deploy" version = "0.1.19" criteria = "safe-to-deploy" -[[exemptions.hex-literal]] -version = "0.1.4" -criteria = "safe-to-deploy" - -[[exemptions.hex-literal-impl]] -version = "0.1.2" -criteria = "safe-to-deploy" - [[exemptions.hmac]] version = "0.11.0" criteria = "safe-to-deploy" @@ -617,14 +598,6 @@ criteria = "safe-to-deploy" version = "0.2.17" criteria = "safe-to-deploy" -[[exemptions.proc-macro-hack]] -version = "0.4.3" -criteria = "safe-to-deploy" - -[[exemptions.proc-macro-hack-impl]] -version = "0.4.3" -criteria = "safe-to-deploy" - [[exemptions.radium]] version = "0.7.0" criteria = "safe-to-deploy" @@ -633,10 +606,6 @@ criteria = "safe-to-deploy" version = "0.8.5" criteria = "safe-to-deploy" -[[exemptions.rand_seeder]] -version = "0.2.3" -criteria = "safe-to-deploy" - [[exemptions.reddsa]] version = "0.3.0" criteria = "safe-to-deploy" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index 26bc4e7d28..13502041eb 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -1115,12 +1115,6 @@ delta = "0.9.0 -> 0.10.0" notes = "I previously reviewed the crypto-sensitive portions of these changes as well." aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.pairing]] -who = "Sean Bowe " -criteria = "safe-to-deploy" -delta = "0.22.0 -> 0.23.0" -aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" - [[audits.zcash.audits.platforms]] who = "Daira Emma Hopwood " criteria = "safe-to-deploy" From 036bdaab213c968e464564528a61d572faf748c6 Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Sat, 20 Apr 2024 13:12:51 -0400 Subject: [PATCH 15/35] Add transaction summary to wallet:post (#4910) --- ironfish-cli/src/commands/wallet/post.ts | 20 +-- ironfish-cli/src/utils/asset.ts | 29 ++++ ironfish-cli/src/utils/transaction.ts | 167 ++++++++++++++++++++++- ironfish/src/utils/currency.ts | 2 +- 4 files changed, 199 insertions(+), 19 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/post.ts b/ironfish-cli/src/commands/wallet/post.ts index 4235410d19..4eb93b4413 100644 --- a/ironfish-cli/src/commands/wallet/post.ts +++ b/ironfish-cli/src/commands/wallet/post.ts @@ -1,17 +1,12 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { - CurrencyUtils, - RawTransaction, - RawTransactionSerde, - RpcClient, - Transaction, -} from '@ironfish/sdk' +import { RawTransaction, RawTransactionSerde, RpcClient, Transaction } from '@ironfish/sdk' import { CliUx, Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' import { longPrompt } from '../../utils/longPrompt' +import { renderRawTransactionDetails } from '../../utils/transaction' export class PostCommand extends IronfishCommand { static summary = 'Post a raw transaction' @@ -118,16 +113,7 @@ export class PostCommand extends IronfishCommand { this.error('Can not find an account to confirm the transaction') } - let spending = 0n - for (const output of raw.outputs) { - spending += output.note.value() - } - - const renderedSpending = CurrencyUtils.render(spending, true) - const renderedFee = CurrencyUtils.render(raw.fee, true) - this.log( - `You are about to post a transaction that sends ${renderedSpending}, with ${raw.mints.length} mints and ${raw.burns.length} burns with a fee ${renderedFee} using account ${account}`, - ) + await renderRawTransactionDetails(client, raw, account, this.logger) return CliUx.ux.confirm('Do you want to post this (Y/N)?') } diff --git a/ironfish-cli/src/utils/asset.ts b/ironfish-cli/src/utils/asset.ts index 4770dd6125..1089f40649 100644 --- a/ironfish-cli/src/utils/asset.ts +++ b/ironfish-cli/src/utils/asset.ts @@ -6,9 +6,11 @@ import { Asset } from '@ironfish/rust-nodejs' import { BufferUtils, CurrencyUtils, + RPC_ERROR_CODES, RpcAsset, RpcAssetVerification, RpcClient, + RpcRequestError, StringUtils, } from '@ironfish/sdk' import chalk from 'chalk' @@ -157,6 +159,33 @@ export async function selectAsset( return response.asset } +export async function getAssetVerificationByIds( + client: Pick, + assetIds: string[], + account: string | undefined, + confirmations: number | undefined, +): Promise<{ [key: string]: RpcAssetVerification }> { + assetIds = [...new Set(assetIds)] + const assets = await Promise.all( + assetIds.map((id) => + client.wallet.getAsset({ id, account, confirmations }).catch((e) => { + if (e instanceof RpcRequestError && e.code === RPC_ERROR_CODES.NOT_FOUND.valueOf()) { + return undefined + } else { + throw e + } + }), + ), + ) + const assetLookup: { [key: string]: RpcAssetVerification } = {} + assets.forEach((asset) => { + if (asset) { + assetLookup[asset.content.id] = asset.content.verification + } + }) + return assetLookup +} + export async function getAssetsByIDs( client: Pick, assetIds: string[], diff --git a/ironfish-cli/src/utils/transaction.ts b/ironfish-cli/src/utils/transaction.ts index dc30a1ffbd..f941a2d0b4 100644 --- a/ironfish-cli/src/utils/transaction.ts +++ b/ironfish-cli/src/utils/transaction.ts @@ -2,7 +2,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { Asset } from '@ironfish/rust-nodejs' import { + assetMetadataWithDefaults, createRootLogger, CurrencyUtils, GetUnsignedTransactionNotesResponse, @@ -17,7 +19,7 @@ import { } from '@ironfish/sdk' import { CliUx } from '@oclif/core' import { ProgressBar } from '../types' -import { getAssetsByIDs } from './asset' +import { getAssetsByIDs, getAssetVerificationByIds } from './asset' export class TransactionTimer { private progressBar: ProgressBar | undefined @@ -249,6 +251,146 @@ export async function renderUnsignedTransactionDetails( logger.log('') } +export async function renderRawTransactionDetails( + client: Pick, + rawTransaction: RawTransaction, + account: string, + logger: Logger, +): Promise { + const assetIds = collectRawTransactionAssetIds(rawTransaction) + const assetLookup = await getAssetVerificationByIds(client, assetIds, account, undefined) + const feeString = CurrencyUtils.render(rawTransaction.fee, true) + // Every transaction except for a miners transaction should have at least 1 spend for the transaction fee + const from = rawTransaction.spends.length ? rawTransaction.spends[0].note.owner() : null + + const summary = `\ +\n=================== +Transaction Summary +=================== + +From ${from} +Fee ${feeString} +Expiration ${ + rawTransaction.expiration !== null ? rawTransaction.expiration.toString() : '' + }` + logger.log(summary) + + if (rawTransaction.mints.length > 0) { + logger.log('') + logger.log('==================') + logger.log(`Mints (${rawTransaction.mints.length})`) + logger.log('==================') + + for (const [i, mint] of rawTransaction.mints.entries()) { + if (i !== 0) { + logger.log('------------------') + } + logger.log('') + + const asset = new Asset(mint.creator, mint.name, mint.metadata) + + const renderedAmount = CurrencyUtils.render( + mint.value, + false, + asset.id().toString('hex'), + assetLookup[asset.id().toString('hex')], + ) + logger.log(`Amount ${renderedAmount}`) + logger.log(`Asset ID ${asset.id().toString('hex')}`) + logger.log(`Name ${mint.name}`) + logger.log(`Metadata ${mint.metadata}`) + + if (mint.transferOwnershipTo) { + logger.log( + `Ownership of asset will be transferred to ${mint.transferOwnershipTo}. The current account will no longer have any permission to mint or modify asset. This action cannot be undone.`, + ) + } + logger.log('') + } + } + + if (rawTransaction.burns.length > 0) { + logger.log('') + logger.log('==================') + logger.log(`Burns (${rawTransaction.burns.length})`) + logger.log('==================') + + for (const [i, burn] of rawTransaction.burns.entries()) { + if (i !== 0) { + logger.log('------------------') + } + logger.log('') + + const renderedAmount = CurrencyUtils.render( + burn.value, + false, + burn.assetId.toString('hex'), + assetLookup[burn.assetId.toString('hex')], + ) + logger.log(`Amount ${renderedAmount}`) + logger.log(`Asset ID ${burn.assetId.toString('hex')}`) + logger.log('') + } + } + + if (rawTransaction.spends.length > 0) { + logger.log('') + logger.log('==================') + logger.log(`Spends (${rawTransaction.spends.length})`) + logger.log('==================') + + for (const [i, { note }] of rawTransaction.spends.entries()) { + if (i !== 0) { + logger.log('------------------') + } + logger.log('') + + const { symbol } = assetMetadataWithDefaults( + note.assetId().toString('hex'), + assetLookup[note.assetId().toString('hex')], + ) + logger.log(`Asset ${symbol}`) + logger.log(`Note Hash ${note.hash().toString('hex')}`) + logger.log('') + } + } + + if (rawTransaction.outputs.length > 0) { + logger.log('') + logger.log('==================') + logger.log( + `Notes (${rawTransaction.outputs.length}) (Additional notes will be added to return unspent assets to the sender)`, + ) + logger.log('==================') + + for (const [i, { note }] of rawTransaction.outputs.entries()) { + if (i !== 0) { + logger.log('------------------') + } + logger.log('') + + const { symbol } = assetMetadataWithDefaults( + note.assetId().toString('hex'), + assetLookup[note.assetId().toString('hex')], + ) + const renderedAmount = CurrencyUtils.render( + note.value(), + false, + note.assetId().toString('hex'), + assetLookup[note.assetId().toString('hex')], + ) + logger.log(`Amount ${renderedAmount}`) + logger.log(`Asset ${symbol}`) + logger.log(`Memo ${note.memo().toString('utf-8')}`) + logger.log(`Recipient ${note.owner()}`) + logger.log(`Sender ${note.sender()}`) + logger.log('') + } + } + + logger.log('') +} + export function displayTransactionSummary( transaction: RawTransaction, asset: RpcAsset, @@ -366,6 +508,29 @@ export async function watchTransaction(options: { } } +function collectRawTransactionAssetIds(rawTransaction: RawTransaction): string[] { + const assetIds = new Set() + + for (const mint of rawTransaction.mints) { + const newAsset = new Asset(mint.creator, mint.name, mint.metadata) + assetIds.add(newAsset.id().toString('hex')) + } + + for (const burn of rawTransaction.burns) { + assetIds.add(burn.assetId.toString('hex')) + } + + for (const spend of rawTransaction.spends) { + assetIds.add(spend.note.assetId().toString('hex')) + } + + for (const output of rawTransaction.outputs) { + assetIds.add(output.note.assetId().toString('hex')) + } + + return Array.from(assetIds) +} + function collectAssetIds( unsignedTransaction: UnsignedTransaction, notes?: GetUnsignedTransactionNotesResponse, diff --git a/ironfish/src/utils/currency.ts b/ironfish/src/utils/currency.ts index 2644cb9661..33dfcc66c2 100644 --- a/ironfish/src/utils/currency.ts +++ b/ironfish/src/utils/currency.ts @@ -171,7 +171,7 @@ export const MAXIMUM_ORE_AMOUNT = 2n ** 64n export const MINIMUM_IRON_AMOUNT = CurrencyUtils.renderIron(MINIMUM_ORE_AMOUNT) export const MAXIMUM_IRON_AMOUNT = CurrencyUtils.renderIron(MAXIMUM_ORE_AMOUNT) -function assetMetadataWithDefaults( +export function assetMetadataWithDefaults( assetId?: string, verifiedAssetMetadata?: { decimals?: number From 3fb06a995817521099c86e3b90b1ae97c76f8f36 Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Mon, 22 Apr 2024 11:23:55 -0400 Subject: [PATCH 16/35] Batch accounts when decrypting transaction notes (#4727) --- ironfish/src/wallet/wallet.ts | 118 ++++++++++++++++++++++------------ 1 file changed, 76 insertions(+), 42 deletions(-) diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index ab77eaf905..1b13d0ab48 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -411,22 +411,25 @@ export class Wallet { async (a) => await this.isAccountUpToDate(a), )) - const decryptedNotesByAccountId = new Map>() - const batchSize = 20 + const notePromises: Array< + Promise> + > = [] + let decryptNotesPayloads = [] for (const account of accountsToCheck) { - const decryptedNotes = [] - let decryptNotesPayloads = [] let currentNoteIndex = initialNoteIndex for (const note of transaction.notes) { decryptNotesPayloads.push({ - serializedNote: note.serialize(), - incomingViewKey: account.incomingViewKey, - outgoingViewKey: account.outgoingViewKey, - viewKey: account.viewKey, - currentNoteIndex, - decryptForSpender, + accountId: account.id, + options: { + serializedNote: note.serialize(), + incomingViewKey: account.incomingViewKey, + outgoingViewKey: account.outgoingViewKey, + viewKey: account.viewKey, + currentNoteIndex, + decryptForSpender, + }, }) if (currentNoteIndex) { @@ -434,35 +437,45 @@ export class Wallet { } if (decryptNotesPayloads.length >= batchSize) { - const decryptedNotesBatch = await this.decryptNotesFromTransaction( - decryptNotesPayloads, - ) - decryptedNotes.push(...decryptedNotesBatch) + notePromises.push(this.decryptNotesFromTransaction(decryptNotesPayloads)) decryptNotesPayloads = [] } } + } - if (decryptNotesPayloads.length) { - const decryptedNotesBatch = await this.decryptNotesFromTransaction(decryptNotesPayloads) - decryptedNotes.push(...decryptedNotesBatch) - } + if (decryptNotesPayloads.length) { + notePromises.push(this.decryptNotesFromTransaction(decryptNotesPayloads)) + } - if (decryptedNotes.length) { - decryptedNotesByAccountId.set(account.id, decryptedNotes) - } + const decryptedNotesByAccountId = new Map>() + const flatPromises = (await Promise.all(notePromises)).flat() + for (const decryptedNoteResponse of flatPromises) { + const accountNotes = decryptedNotesByAccountId.get(decryptedNoteResponse.accountId) ?? [] + accountNotes.push(decryptedNoteResponse.decryptedNote) + decryptedNotesByAccountId.set(decryptedNoteResponse.accountId, accountNotes) } return decryptedNotesByAccountId } async decryptNotesFromTransaction( - decryptNotesPayloads: Array, - ): Promise> { - const decryptedNotes = [] - const response = await this.workerPool.decryptNotes(decryptNotesPayloads) - for (const decryptedNote of response) { + decryptNotesPayloads: Array<{ accountId: string; options: DecryptNoteOptions }>, + ): Promise> { + const decryptedNotes: Array<{ accountId: string; decryptedNote: DecryptedNote }> = [] + const response = await this.workerPool.decryptNotes( + decryptNotesPayloads.map((p) => p.options), + ) + + // Job should return same number of nullable notes as requests + Assert.isEqual(response.length, decryptNotesPayloads.length) + + for (let i = 0; i < response.length; i++) { + const decryptedNote = response[i] if (decryptedNote) { - decryptedNotes.push(decryptedNote) + decryptedNotes.push({ + accountId: decryptNotesPayloads[i].accountId, + decryptedNote, + }) } } @@ -484,9 +497,34 @@ export class Wallet { } }) - for (const account of accounts) { - const shouldDecrypt = await this.shouldDecryptForAccount(blockHeader, account) + const shouldDecryptAccounts = await AsyncUtils.filter(accounts, (a) => + this.shouldDecryptForAccount(blockHeader, a), + ) + const shouldDecryptAccountIds = new Set(shouldDecryptAccounts.map((a) => a.id)) + + const decryptedTransactions = await Promise.all( + transactions.map(({ transaction, initialNoteIndex }) => + this.decryptNotes(transaction, initialNoteIndex, false, shouldDecryptAccounts).then( + (r) => ({ + result: r, + transaction, + }), + ), + ), + ) + + // account id -> transaction hash -> Array + const decryptedNotesMap: Map>> = new Map() + for (const { transaction, result } of decryptedTransactions) { + for (const [accountId, decryptedNotes] of result) { + const accountTxnsMap = + decryptedNotesMap.get(accountId) ?? new BufferMap>() + accountTxnsMap.set(transaction.hash(), decryptedNotes) + decryptedNotesMap.set(accountId, accountTxnsMap) + } + } + for (const account of accounts) { if (scan && scan.isAborted) { scan.signalComplete() this.scan = null @@ -495,11 +533,16 @@ export class Wallet { await this.walletDb.db.transaction(async (tx) => { let assetBalanceDeltas = new AssetBalances() + const accountTxnsMap = decryptedNotesMap.get(account.id) + const txns = transactions.map((t) => ({ + transaction: t.transaction, + decryptedNotes: accountTxnsMap?.get(t.transaction.hash()) ?? [], + })) - if (shouldDecrypt) { + if (shouldDecryptAccountIds.has(account.id)) { assetBalanceDeltas = await this.connectBlockTransactions( blockHeader, - transactions, + txns, account, scan, tx, @@ -556,27 +599,18 @@ export class Wallet { private async connectBlockTransactions( blockHeader: WalletBlockHeader, - transactions: WalletBlockTransaction[], + transactions: Array<{ transaction: Transaction; decryptedNotes: Array }>, account: Account, scan?: ScanState, tx?: IDatabaseTransaction, ): Promise { const assetBalanceDeltas = new AssetBalances() - for (const { transaction, initialNoteIndex } of transactions) { + for (const { transaction, decryptedNotes } of transactions) { if (scan && scan.isAborted) { return assetBalanceDeltas } - const decryptedNotesByAccountId = await this.decryptNotes( - transaction, - initialNoteIndex, - false, - [account], - ) - - const decryptedNotes = decryptedNotesByAccountId.get(account.id) ?? [] - const transactionDeltas = await account.connectTransaction( blockHeader, transaction, From 4ed09dbc9f2d6c29449782bad84bd7015b00d5f2 Mon Sep 17 00:00:00 2001 From: Daniel Cogan Date: Mon, 22 Apr 2024 11:30:31 -0400 Subject: [PATCH 17/35] Show the correct decimal amount in wallet:transactions command (#4913) --- .../src/commands/wallet/transactions.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/transactions.ts b/ironfish-cli/src/commands/wallet/transactions.ts index 243e7074a7..1257845ff6 100644 --- a/ironfish-cli/src/commands/wallet/transactions.ts +++ b/ironfish-cli/src/commands/wallet/transactions.ts @@ -155,23 +155,24 @@ export class TransactionsCommand extends IronfishCommand { const group = this.getRowGroup(index, assetCount, transactionRows.length) + const transactionRow = { + group, + assetId, + assetName: asset.name, + amount, + assetDecimals: asset.verification.decimals, + assetSymbol: asset.verification.symbol, + } + // include full transaction details in first row or non-cli-formatted output if (transactionRows.length === 0 || format !== Format.cli) { transactionRows.push({ ...transaction, - group, - assetId, - assetName: asset.name, - amount, + ...transactionRow, feePaid, }) } else { - transactionRows.push({ - group, - assetId, - assetName: asset.name, - amount, - }) + transactionRows.push(transactionRow) } } From a90ee451cb1a721d8173f4f89382060dedd5a394 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Mon, 22 Apr 2024 10:52:51 -0700 Subject: [PATCH 18/35] select secret name from list in dkg commands (#4916) * select secret name from list in dkg commands if no secretName is passed via flags allow the user to select one of their existing names from a list in round1 allow users to create a new secret name from the list makes it easier for users to know which secret names they have and can use in the context of the dkg commands. allows users to start the dkg process without running the participant:create command first * removes option to create new secret during round1 identities must be shared with all participants before round1, so secret must be created before round1 --- .../commands/wallet/multisig/dkg/round1.ts | 9 +++--- .../commands/wallet/multisig/dkg/round2.ts | 15 ++++++---- .../commands/wallet/multisig/dkg/round3.ts | 15 ++++++---- ironfish-cli/src/utils/multisig.ts | 30 ++++++++++++++++++- 4 files changed, 53 insertions(+), 16 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts index b14a23a87b..eaa165f37c 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts @@ -5,6 +5,7 @@ import { CliUx, Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' import { longPrompt } from '../../../../utils/longPrompt' +import { selectSecret } from '../../../../utils/multisig' export class DkgRound1Command extends IronfishCommand { static description = 'Perform round1 of the DKG protocol for multisig account creation' @@ -31,11 +32,11 @@ export class DkgRound1Command extends IronfishCommand { async start(): Promise { const { flags } = await this.parse(DkgRound1Command) + const client = await this.sdk.connectRpc() + let secretName = flags.secretName if (!secretName) { - secretName = await CliUx.ux.prompt('Enter the name of the secret to use', { - required: true, - }) + secretName = await selectSecret(client) } let identities = flags.identity @@ -62,8 +63,6 @@ export class DkgRound1Command extends IronfishCommand { } } - const client = await this.sdk.connectRpc() - const response = await client.wallet.multisig.dkg.round1({ secretName: secretName, participants: identities.map((identity) => ({ identity })), diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts index 914fe74f2e..220c3f8b54 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts @@ -5,6 +5,7 @@ import { CliUx, Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' import { longPrompt } from '../../../../utils/longPrompt' +import { selectSecret } from '../../../../utils/multisig' export class DkgRound2Command extends IronfishCommand { static description = 'Perform round2 of the DKG protocol for multisig account creation' @@ -15,7 +16,6 @@ export class DkgRound2Command extends IronfishCommand { secretName: Flags.string({ char: 's', description: 'The name of the secret to use for encryption during DKG', - required: true, }), encryptedSecretPackage: Flags.string({ char: 'e', @@ -32,10 +32,17 @@ export class DkgRound2Command extends IronfishCommand { async start(): Promise { const { flags } = await this.parse(DkgRound2Command) + const client = await this.sdk.connectRpc() + + let secretName = flags.secretName + if (!secretName) { + secretName = await selectSecret(client) + } + let encryptedSecretPackage = flags.encryptedSecretPackage if (!encryptedSecretPackage) { encryptedSecretPackage = await CliUx.ux.prompt( - `Enter the encrypted secret package for secret ${flags.secretName}`, + `Enter the encrypted secret package for secret ${secretName}`, { required: true, }, @@ -60,10 +67,8 @@ export class DkgRound2Command extends IronfishCommand { } publicPackages = publicPackages.map((i) => i.trim()) - const client = await this.sdk.connectRpc() - const response = await client.wallet.multisig.dkg.round2({ - secretName: flags.secretName, + secretName, encryptedSecretPackage, publicPackages, }) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts index fadc6b15c6..fca6c9cfa3 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -5,6 +5,7 @@ import { CliUx, Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' import { longPrompt } from '../../../../utils/longPrompt' +import { selectSecret } from '../../../../utils/multisig' export class DkgRound3Command extends IronfishCommand { static description = 'Perform round3 of the DKG protocol for multisig account creation' @@ -15,7 +16,6 @@ export class DkgRound3Command extends IronfishCommand { secretName: Flags.string({ char: 's', description: 'The name of the secret to use for decryption during DKG', - required: true, }), accountName: Flags.string({ char: 'n', @@ -42,10 +42,17 @@ export class DkgRound3Command extends IronfishCommand { async start(): Promise { const { flags } = await this.parse(DkgRound3Command) + const client = await this.sdk.connectRpc() + + let secretName = flags.secretName + if (!secretName) { + secretName = await selectSecret(client) + } + let round2SecretPackage = flags.round2SecretPackage if (!round2SecretPackage) { round2SecretPackage = await CliUx.ux.prompt( - `Enter the encrypted secret package for secret ${flags.secretName}`, + `Enter the encrypted secret package for secret ${secretName}`, { required: true, }, @@ -88,10 +95,8 @@ export class DkgRound3Command extends IronfishCommand { } round2PublicPackages = round2PublicPackages.map((i) => i.trim()) - const client = await this.sdk.connectRpc() - const response = await client.wallet.multisig.dkg.round3({ - secretName: flags.secretName, + secretName: secretName, accountName: flags.accountName, round2SecretPackage, round1PublicPackages, diff --git a/ironfish-cli/src/utils/multisig.ts b/ironfish-cli/src/utils/multisig.ts index f41367eefc..97791419f0 100644 --- a/ironfish-cli/src/utils/multisig.ts +++ b/ironfish-cli/src/utils/multisig.ts @@ -1,7 +1,8 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { FileSystem, YupUtils } from '@ironfish/sdk' +import { FileSystem, RpcClient, YupUtils } from '@ironfish/sdk' +import inquirer from 'inquirer' import * as yup from 'yup' export type MultisigTransactionOptions = { @@ -68,3 +69,30 @@ export const MultisigTransactionJson = { load, resolveFlags, } + +export async function selectSecret(client: Pick): Promise { + const identitiesResponse = await client.wallet.multisig.getIdentities() + + const choices = [] + for (const { name } of identitiesResponse.content.identities) { + choices.push({ + name, + value: name, + }) + } + + choices.sort((a, b) => a.name.localeCompare(b.name)) + + const selection = await inquirer.prompt<{ + name: string + }>([ + { + name: 'name', + message: 'Select participant secret name', + type: 'list', + choices, + }, + ]) + + return selection.name +} From 307c88a27caa4bbbcaa9187dfcc668b601d51758 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Mon, 22 Apr 2024 15:14:02 -0400 Subject: [PATCH 19/35] feat(cli): Improve prompt in round3 to indicate which packages (#4917) --- ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts index fca6c9cfa3..4ab30241b5 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -62,7 +62,7 @@ export class DkgRound3Command extends IronfishCommand { let round1PublicPackages = flags.round1PublicPackages if (!round1PublicPackages || round1PublicPackages.length < 2) { const input = await longPrompt( - 'Enter public packages separated by commas, one for each participant', + 'Enter round 1 public packages separated by commas, one for each participant', { required: true, }, @@ -80,7 +80,7 @@ export class DkgRound3Command extends IronfishCommand { let round2PublicPackages = flags.round2PublicPackages if (!round2PublicPackages) { const input = await longPrompt( - 'Enter public packages separated by commas, one for each participant', + 'Enter round 2 public packages separated by commas, one for each participant', { required: true, }, From ef0e28d97cbe7c47c9ffcd21bfd27ac1ab0983ea Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 22 Apr 2024 12:10:46 -0700 Subject: [PATCH 20/35] Multisig DKG Round 3: add an option to rescan the imported account --- ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts index dad6e3e2c1..e74660c615 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts @@ -16,6 +16,7 @@ export type DkgRound3Request = { round1PublicPackages: Array round2PublicPackages: Array accountName?: string + rescan?: boolean } export type DkgRound3Response = { @@ -30,6 +31,7 @@ export const DkgRound3RequestSchema: yup.ObjectSchema = yup round1PublicPackages: yup.array().of(yup.string().defined()).defined(), round2PublicPackages: yup.array().of(yup.string().defined()).defined(), accountName: yup.string().optional(), + rescan: yup.boolean().optional().default(false), }) .defined() @@ -94,7 +96,13 @@ routes.register( const account = await node.wallet.importAccount(accountImport) - // TODO: add an option to skip rescan + if (request.data.rescan) { + if (node.wallet.nodeClient) { + void node.wallet.scanTransactions(undefined, true) + } + } else { + await node.wallet.skipRescan(account) + } request.end({ name: account.name, From 0da75f2c4d28246386b404513b3f476d36f2d29b Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:22:51 -0700 Subject: [PATCH 21/35] removes 'next step' from participant:create output (#4921) the next step instruction refers to the dealer in trusted dealer keygen, so it doesn't always apply now that we support dkg --- .../src/commands/wallet/multisig/participant/create.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/participant/create.ts b/ironfish-cli/src/commands/wallet/multisig/participant/create.ts index fb4c72502e..b384bbab67 100644 --- a/ironfish-cli/src/commands/wallet/multisig/participant/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/participant/create.ts @@ -49,9 +49,5 @@ export class MultisigIdentityCreate extends IronfishCommand { this.log('Identity:') this.log(response.content.identity) - - this.log() - this.log('Next step:') - this.log('Send the identity to the multisig account dealer.') } } From 4ab45df032efa43456438db24d3f1dbe87eb31d5 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 22 Apr 2024 14:06:13 -0700 Subject: [PATCH 22/35] Multisig: fetch participant identities from the public key package When importing a multisig account, participant identities are stored in two places: 1. the public key package (part of the account record in the walletdb) 2. on dedicated records in the walletdb The #2 records can sometimes get lost. This is the case currently of account rescans. Because there is no future development scheduled that will benefit from #2, it's easier to just ignore those records and instead rely only on the public key package. --- .../multisig/createSignatureShare.test.ts | 17 +++++++---- .../wallet/multisig/createSignatureShare.ts | 8 ++---- .../wallet/multisig/getAccountIdentities.ts | 8 ++---- ironfish/src/wallet/account/account.ts | 7 +++++ ironfish/src/wallet/wallet.test.slow.ts | 6 +--- ironfish/src/wallet/wallet.ts | 10 ------- ironfish/src/wallet/walletdb/walletdb.test.ts | 20 ------------- ironfish/src/wallet/walletdb/walletdb.ts | 28 ------------------- 8 files changed, 25 insertions(+), 79 deletions(-) diff --git a/ironfish/src/rpc/routes/wallet/multisig/createSignatureShare.test.ts b/ironfish/src/rpc/routes/wallet/multisig/createSignatureShare.test.ts index d9917fa587..1091ab900c 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/createSignatureShare.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/createSignatureShare.test.ts @@ -5,7 +5,7 @@ import { generateKey } from '@ironfish/rust-nodejs' import { Assert } from '../../../../assert' import { useAccountAndAddFundsFixture, useUnsignedTxFixture } from '../../../../testUtilities' import { createRouteTest } from '../../../../testUtilities/routeTest' -import { ACCOUNT_SCHEMA_VERSION } from '../../../../wallet' +import { ACCOUNT_SCHEMA_VERSION, AssertMultisig } from '../../../../wallet' describe('Route wallt/multisig/createSignatureShare', () => { const routeTest = createRouteTest() @@ -108,15 +108,22 @@ describe('Route wallt/multisig/createSignatureShare', () => { }) ).content.signingPackage - // Remove one participant from the participants store to simulate unknown signer + // Alter the public key package to replace one identity with another, so + // that we can later pretend that we created a signature share from an + // unknown identity const account = routeTest.wallet.getAccountByName(accountNames[0]) Assert.isNotNull(account) + AssertMultisig(account) - await routeTest.wallet.walletDb.deleteParticipantIdentity( - account, - Buffer.from(participants[1].identity, 'hex'), + const fromIdentity = participants[1].identity + const toIdentity = participants[2].identity + account.multisigKeys.publicKeyPackage = account.multisigKeys.publicKeyPackage.replace( + fromIdentity, + toIdentity, ) + await routeTest.wallet.walletDb.setAccount(account) + // Attempt to create signature share await expect( routeTest.client.wallet.multisig.createSignatureShare({ diff --git a/ironfish/src/rpc/routes/wallet/multisig/createSignatureShare.ts b/ironfish/src/rpc/routes/wallet/multisig/createSignatureShare.ts index 5e7db581d8..29141a3166 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/createSignatureShare.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/createSignatureShare.ts @@ -4,7 +4,6 @@ import { multisig } from '@ironfish/rust-nodejs' import { BufferSet } from 'buffer-map' import * as yup from 'yup' -import { AsyncUtils } from '../../../../utils' import { AssertMultisigSigner } from '../../../../wallet' import { RpcValidationError } from '../../../adapters' import { ApiNamespace } from '../../namespaces' @@ -39,7 +38,7 @@ export const CreateSignatureShareResponseSchema: yup.ObjectSchema( `${ApiNamespace.wallet}/multisig/createSignatureShare`, CreateSignatureShareRequestSchema, - async (request, node): Promise => { + (request, node) => { AssertHasRpcContext(request, node, 'wallet') const account = getAccount(node.wallet, request.data.account) @@ -49,10 +48,7 @@ routes.register identity.toString('hex')) + const identities = account + .getMultisigParticipantIdentities() + .map((identity) => identity.toString('hex')) request.end({ identities }) }, diff --git a/ironfish/src/wallet/account/account.ts b/ironfish/src/wallet/account/account.ts index 565e5352a3..90a9a6c509 100644 --- a/ironfish/src/wallet/account/account.ts +++ b/ironfish/src/wallet/account/account.ts @@ -1,6 +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 { multisig } from '@ironfish/rust-nodejs' import { Asset } from '@ironfish/rust-nodejs' import { BufferMap, BufferSet } from 'buffer-map' import MurmurHash3 from 'imurmurhash' @@ -1282,6 +1283,12 @@ export class Account { return notes } + + getMultisigParticipantIdentities(): Array { + AssertMultisig(this) + const publicKeyPackage = new multisig.PublicKeyPackage(this.multisigKeys.publicKeyPackage) + return publicKeyPackage.identities() + } } export function calculateAccountPrefix(id: string): Buffer { diff --git a/ironfish/src/wallet/wallet.test.slow.ts b/ironfish/src/wallet/wallet.test.slow.ts index b5f73e2c01..7ab1e396a4 100644 --- a/ironfish/src/wallet/wallet.test.slow.ts +++ b/ironfish/src/wallet/wallet.test.slow.ts @@ -1362,11 +1362,7 @@ describe('Wallet', () => { ...trustedDealerPackage, }) - const storedIdentities: string[] = [] - for await (const identity of node.wallet.walletDb.getParticipantIdentities(account)) { - storedIdentities.push(identity.toString('hex')) - } - + const storedIdentities = account.getMultisigParticipantIdentities() expect(identities.sort()).toEqual(storedIdentities.sort()) }) }) diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index 1b13d0ab48..161d88d2f3 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -1640,16 +1640,6 @@ export class Wallet { } else { await account.updateHead(null, tx) } - - if (account.multisigKeys) { - const publicKeyPackage = new multisig.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/walletdb.test.ts b/ironfish/src/wallet/walletdb/walletdb.test.ts index ac0f5bd6b9..751117ec33 100644 --- a/ironfish/src/wallet/walletdb/walletdb.test.ts +++ b/ironfish/src/wallet/walletdb/walletdb.test.ts @@ -410,24 +410,4 @@ describe('WalletDB', () => { expect(storedSecret.secret).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 = multisig.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 ec90c3d1c6..b494087d38 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -1297,32 +1297,4 @@ export class WalletDB { yield value } } - - 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 - } - } } From 17b9d680fc6b230630c3d081f53c978da76b5303 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 22 Apr 2024 13:25:09 -0700 Subject: [PATCH 23/35] Multisig DKG: test that, at the end of round 3, participant identities are recorded correctly --- .../routes/wallet/multisig/dkg/round3.test.ts | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts index 9d8d2dbfd4..b9a156bb51 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts @@ -1,7 +1,9 @@ /* 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 { Assert } from '../../../../../assert' import { createRouteTest } from '../../../../../testUtilities/routeTest' +import { AsyncUtils } from '../../../../../utils' function removeOneElement(array: Array): Array { const newArray = [...array] @@ -15,6 +17,7 @@ describe('Route multisig/dkg/round3', () => { it('should create round 3 packages', async () => { const secretNames = ['secret-0', 'secret-1', 'secret-2'] + const accountNames = ['account-0', 'account-1', 'account-2'] // Create participants and retrieve their identities await Promise.all( @@ -48,18 +51,12 @@ describe('Route multisig/dkg/round3', () => { ), ) - // Only override 2/3 names - const secretNamesToName = { - [secretNames[0]]: 'foo', - [secretNames[2]]: 'bar', - } - // Perform DKG round 3 const round3Responses = await Promise.all( secretNames.map((secretName, index) => routeTest.client.wallet.multisig.dkg.round3({ secretName, - accountName: secretNamesToName[secretName], + accountName: accountNames[index], round2SecretPackage: round2Packages[index].content.encryptedSecretPackage, round1PublicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), round2PublicPackages: round2Packages.flatMap((pkg) => @@ -75,12 +72,10 @@ describe('Route multisig/dkg/round3', () => { // Check that all accounts that got imported after round 3 have the same public address const publicKeys = await Promise.all( - secretNames.map( + accountNames.map( async (account) => ( - await routeTest.client.wallet.getAccountPublicKey({ - account: secretNamesToName[account] ?? account, - }) + await routeTest.client.wallet.getAccountPublicKey({ account }) ).content.publicKey, ), ) @@ -92,11 +87,25 @@ describe('Route multisig/dkg/round3', () => { // Check all the responses match expect(round3Responses).toHaveLength(publicKeys.length) for (let i = 0; i < round3Responses.length; i++) { - expect(round3Responses[i].content.name).toEqual( - secretNamesToName[secretNames[i]] ?? secretNames[i], - ) + expect(round3Responses[i].content.name).toEqual(accountNames[i]) expect(round3Responses[i].content.publicAddress).toEqual(publicKeys[i]) } + + // Check that the imported accounts all know about other participants' + // identities + const expectedIdentities = participants.map(({ identity }) => identity) + expectedIdentities.sort() + for (const accountName of accountNames) { + const account = routeTest.wallet.getAccountByName(accountName) + Assert.isNotNull(account) + const knownIdentities = ( + await AsyncUtils.materialize( + routeTest.wallet.walletDb.getParticipantIdentities(account), + ) + ).map((identity) => identity.toString('hex')) + knownIdentities.sort() + expect(knownIdentities).toStrictEqual(expectedIdentities) + } }) it('should fail if not all round 1 packages are passed as an input', async () => { From 424bbefc421143c53a59138aa51bdcdc126f2523 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 23 Apr 2024 03:15:28 -0700 Subject: [PATCH 24/35] Fix `wallet.test.slow.ts` and `round3.test.ts` `wallet.test.slow.ts` was broken by 4ab45df0, which inadvertently removed a type conversion step in the test. `round3.test.ts` was broken by merging 17b9d680 on top of 4ab45df0 without proper rebasing. --- .../rpc/routes/wallet/multisig/dkg/round3.test.ts | 14 +++++--------- ironfish/src/wallet/wallet.test.slow.ts | 4 +++- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts index b9a156bb51..fa7078ab5b 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts @@ -3,7 +3,6 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { Assert } from '../../../../../assert' import { createRouteTest } from '../../../../../testUtilities/routeTest' -import { AsyncUtils } from '../../../../../utils' function removeOneElement(array: Array): Array { const newArray = [...array] @@ -93,17 +92,14 @@ describe('Route multisig/dkg/round3', () => { // Check that the imported accounts all know about other participants' // identities - const expectedIdentities = participants.map(({ identity }) => identity) - expectedIdentities.sort() + const expectedIdentities = participants.map(({ identity }) => identity).sort() for (const accountName of accountNames) { const account = routeTest.wallet.getAccountByName(accountName) Assert.isNotNull(account) - const knownIdentities = ( - await AsyncUtils.materialize( - routeTest.wallet.walletDb.getParticipantIdentities(account), - ) - ).map((identity) => identity.toString('hex')) - knownIdentities.sort() + const knownIdentities = account + .getMultisigParticipantIdentities() + .map((identity) => identity.toString('hex')) + .sort() expect(knownIdentities).toStrictEqual(expectedIdentities) } }) diff --git a/ironfish/src/wallet/wallet.test.slow.ts b/ironfish/src/wallet/wallet.test.slow.ts index 7ab1e396a4..8c6bd223dd 100644 --- a/ironfish/src/wallet/wallet.test.slow.ts +++ b/ironfish/src/wallet/wallet.test.slow.ts @@ -1362,7 +1362,9 @@ describe('Wallet', () => { ...trustedDealerPackage, }) - const storedIdentities = account.getMultisigParticipantIdentities() + const storedIdentities = account + .getMultisigParticipantIdentities() + .map((identity) => identity.toString('hex')) expect(identities.sort()).toEqual(storedIdentities.sort()) }) }) From e249009698eb5750e55fd6f891b4a517820626a5 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 23 Apr 2024 03:08:23 -0700 Subject: [PATCH 25/35] Multisig DKG: use the round 2 "combined" public packages --- Cargo.lock | 2 +- .../commands/wallet/multisig/dkg/round2.ts | 13 +++----- .../commands/wallet/multisig/dkg/round3.ts | 11 +++++-- ironfish-rust-nodejs/index.d.ts | 6 +--- ironfish-rust-nodejs/src/multisig.rs | 20 ++----------- .../rpc/routes/wallet/multisig/dkg/round2.ts | 13 ++------ .../routes/wallet/multisig/dkg/round3.test.ts | 30 +++++-------------- .../wallet/multisig/integration.test.slow.ts | 8 +---- 8 files changed, 29 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a5739e7477..04f6df2125 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1493,7 +1493,7 @@ dependencies = [ [[package]] name = "ironfish-frost" version = "0.1.0" -source = "git+https://github.com/iron-fish/ironfish-frost.git?branch=main#e432d1fc6f466b19cb01231ff20443d9bcb182a5" +source = "git+https://github.com/iron-fish/ironfish-frost.git?branch=main#d2b082e3cd25d12073c1b113da941960c08fcb32" dependencies = [ "blake3", "chacha20 0.9.1", diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts index 220c3f8b54..c966575fea 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts @@ -77,17 +77,12 @@ export class DkgRound2Command extends IronfishCommand { this.log(response.content.encryptedSecretPackage) this.log() - this.log('\nPublic Packages:\n') - for (const { recipientIdentity, publicPackage } of response.content.publicPackages) { - this.log('Recipient Identity') - this.log(recipientIdentity) - this.log('----------------') - this.log(publicPackage) - this.log() - } + this.log('\nPublic Package:\n') + this.log(response.content.publicPackages) + this.log() this.log() this.log('Next step:') - this.log('Send each public package to the participant with the matching identity') + this.log('Send the public package to each participant') } } diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts index 4ab30241b5..6cebfebe91 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -87,9 +87,16 @@ export class DkgRound3Command extends IronfishCommand { ) round2PublicPackages = input.split(',') - if (round2PublicPackages.length !== round1PublicPackages.length - 1) { + // Our own public package is optional in this step (if provided, it will + // be ignored), so we can accept both `n` and `n-1` packages + if ( + round2PublicPackages.length < round1PublicPackages.length - 1 || + round2PublicPackages.length > round1PublicPackages.length + ) { + // Suggest to provide `n-1` packages; don't mention the `n` case to + // avoid making the error message too hard to decipher. this.error( - 'The number of round 2 public packages must be 1 less than the number of round 1 public packages', + 'The number of round 2 public packages should be 1 less than the number of round 1 public packages', ) } } diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index e60a175db8..42ce9f89b0 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -254,13 +254,9 @@ export namespace multisig { publicPackage: string } export function dkgRound2(secret: string, encryptedSecretPackage: string, publicPackages: Array): DkgRound2Packages - export interface DkgRound2PublicPackage { - recipientIdentity: string - publicPackage: string - } export interface DkgRound2Packages { encryptedSecretPackage: string - publicPackages: Array + publicPackages: string } export function dkgRound3(secret: ParticipantSecret, round2SecretPackage: string, round1PublicPackages: Array, round2PublicPackages: Array): DkgRound3Packages export interface DkgRound3Packages { diff --git a/ironfish-rust-nodejs/src/multisig.rs b/ironfish-rust-nodejs/src/multisig.rs index 307d9d9f2f..5e30384914 100644 --- a/ironfish-rust-nodejs/src/multisig.rs +++ b/ironfish-rust-nodejs/src/multisig.rs @@ -418,30 +418,16 @@ pub fn dkg_round2( ) .map_err(to_napi_err)?; - let public_packages = public_packages - .iter() - .map(|p| DkgRound2PublicPackage { - recipient_identity: bytes_to_hex(&p.recipient_identity().serialize()), - public_package: bytes_to_hex(&p.serialize()), - }) - .collect(); - Ok(DkgRound2Packages { encrypted_secret_package: bytes_to_hex(&encrypted_secret_package), - public_packages, + public_packages: bytes_to_hex(&public_packages.serialize()), }) } -#[napi(object, namespace = "multisig")] -pub struct DkgRound2PublicPackage { - pub recipient_identity: String, - pub public_package: String, -} - #[napi(object, namespace = "multisig")] pub struct DkgRound2Packages { pub encrypted_secret_package: String, - pub public_packages: Vec, + pub public_packages: String, } #[napi(object, namespace = "multisig")] @@ -456,7 +442,7 @@ pub fn dkg_round3( dkg::round1::PublicPackage::deserialize_from(bytes) })?; let round2_public_packages = try_deserialize(round2_public_packages, |bytes| { - dkg::round2::PublicPackage::deserialize_from(bytes) + dkg::round2::CombinedPublicPackage::deserialize_from(bytes) })?; let (key_package, public_key_package, group_secret_key) = dkg::round3::round3( diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.ts index 2b5aff4f1b..5652ffb395 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.ts @@ -16,7 +16,7 @@ export type DkgRound2Request = { export type DkgRound2Response = { encryptedSecretPackage: string - publicPackages: Array<{ recipientIdentity: string; publicPackage: string }> + publicPackages: string } export const DkgRound2RequestSchema: yup.ObjectSchema = yup @@ -30,16 +30,7 @@ export const DkgRound2RequestSchema: yup.ObjectSchema = yup export const DkgRound2ResponseSchema: yup.ObjectSchema = yup .object({ encryptedSecretPackage: yup.string().defined(), - publicPackages: yup - .array( - yup - .object({ - recipientIdentity: yup.string().defined(), - publicPackage: yup.string().defined(), - }) - .defined(), - ) - .defined(), + publicPackages: yup.string().defined(), }) .defined() diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts index fa7078ab5b..5004060802 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts @@ -58,13 +58,7 @@ describe('Route multisig/dkg/round3', () => { accountName: accountNames[index], round2SecretPackage: round2Packages[index].content.encryptedSecretPackage, round1PublicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), - round2PublicPackages: round2Packages.flatMap((pkg) => - pkg.content.publicPackages - .filter( - ({ recipientIdentity }) => recipientIdentity === participants[index].identity, - ) - .map(({ publicPackage }) => publicPackage), - ), + round2PublicPackages: round2Packages.map((pkg) => pkg.content.publicPackages), }), ), ) @@ -147,11 +141,7 @@ describe('Route multisig/dkg/round3', () => { round1PublicPackages: removeOneElement( round1Packages.map((pkg) => pkg.content.publicPackage), ), - round2PublicPackages: round2Packages.flatMap((pkg) => - pkg.content.publicPackages - .filter(({ recipientIdentity }) => recipientIdentity === participants[0].identity) - .map(({ publicPackage }) => publicPackage), - ), + round2PublicPackages: round2Packages.map((pkg) => pkg.content.publicPackages), }), ).rejects.toThrow('invalid input: expected 3 round 1 public packages, got 2') }) @@ -197,12 +187,12 @@ describe('Route multisig/dkg/round3', () => { secretName: secretNames[0], round2SecretPackage: round2Packages[0].content.encryptedSecretPackage, round1PublicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), + // Here we cannot just remove any one element to perform this test, + // because `round2Packages[0]` does not contain any useful + // information for `secretName[0]`, hence if that gets removed, the + // operation won't fail. This is why we call `slice()` round2PublicPackages: removeOneElement( - round2Packages.flatMap((pkg) => - pkg.content.publicPackages - .filter(({ recipientIdentity }) => recipientIdentity === participants[0].identity) - .map(({ publicPackage }) => publicPackage), - ), + round2Packages.slice(1).map((pkg) => pkg.content.publicPackages), ), }), ).rejects.toThrow('invalid input: expected 2 round 2 public packages, got 1') @@ -249,11 +239,7 @@ describe('Route multisig/dkg/round3', () => { secretName: secretNames[0], round2SecretPackage: round2Packages[1].content.encryptedSecretPackage, round1PublicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), - round2PublicPackages: round2Packages.flatMap((pkg) => - pkg.content.publicPackages - .filter(({ recipientIdentity }) => recipientIdentity === participants[0].identity) - .map(({ publicPackage }) => publicPackage), - ), + round2PublicPackages: round2Packages.map((pkg) => pkg.content.publicPackages), }), ).rejects.toThrow('decryption error: aead::Error') }) diff --git a/ironfish/src/rpc/routes/wallet/multisig/integration.test.slow.ts b/ironfish/src/rpc/routes/wallet/multisig/integration.test.slow.ts index bad0c84265..391c176918 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/integration.test.slow.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/integration.test.slow.ts @@ -205,13 +205,7 @@ describe('multisig RPC integration', () => { secretName: name, round2SecretPackage: round2Packages[index].content.encryptedSecretPackage, round1PublicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), - round2PublicPackages: round2Packages.flatMap((pkg) => - pkg.content.publicPackages - .filter( - ({ recipientIdentity }) => recipientIdentity === participants[index].identity, - ) - .map(({ publicPackage }) => publicPackage), - ), + round2PublicPackages: round2Packages.map((pkg) => pkg.content.publicPackages), }) const participantAccount = routeTest.wallet.getAccountByName(name) From f465b4e46a0c0de719655d4b14ef874d61ae3ade Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 23 Apr 2024 11:11:14 -0700 Subject: [PATCH 26/35] Multisig DKG: prefix every occurrence of "package" with the round number --- .../commands/wallet/multisig/dkg/round1.ts | 10 ++-- .../commands/wallet/multisig/dkg/round2.ts | 52 +++++++++---------- .../commands/wallet/multisig/dkg/round3.ts | 10 ++-- ironfish-rust-nodejs/index.d.ts | 10 ++-- ironfish-rust-nodejs/src/multisig.rs | 33 ++++++------ .../routes/wallet/multisig/dkg/round1.test.ts | 12 ++--- .../rpc/routes/wallet/multisig/dkg/round1.ts | 8 +-- .../routes/wallet/multisig/dkg/round2.test.ts | 14 ++--- .../rpc/routes/wallet/multisig/dkg/round2.ts | 20 +++---- .../routes/wallet/multisig/dkg/round3.test.ts | 40 +++++++------- .../wallet/multisig/integration.test.slow.ts | 10 ++-- 11 files changed, 107 insertions(+), 112 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts index eaa165f37c..dfc4f012e8 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts @@ -69,15 +69,15 @@ export class DkgRound1Command extends IronfishCommand { minSigners: minSigners, }) - this.log('\nEncrypted Secret Package:\n') - this.log(response.content.encryptedSecretPackage) + this.log('\nRound 1 Encrypted Secret Package:\n') + this.log(response.content.round1SecretPackage) this.log() - this.log('\nPublic Package:\n') - this.log(response.content.publicPackage) + this.log('\nRound 1 Public Package:\n') + this.log(response.content.round1PublicPackage) this.log() this.log('Next step:') - this.log('Send the public package to each participant') + this.log('Send the round 1 public package to each participant') } } diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts index c966575fea..2f1725ecd2 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts @@ -17,14 +17,14 @@ export class DkgRound2Command extends IronfishCommand { char: 's', description: 'The name of the secret to use for encryption during DKG', }), - encryptedSecretPackage: Flags.string({ + round1SecretPackage: Flags.string({ char: 'e', - description: 'The ecrypted secret package created during DKG round1', + description: 'The encrypted secret package created during DKG round 1', }), - publicPackage: Flags.string({ + round1PublicPackages: Flags.string({ char: 'p', description: - 'The public package that a participant generated during DKG round1 (may be specified multiple times for multiple participants). Must include your own round1 public package', + 'The public packages that each participant generated during DKG round 1 (may be specified multiple times for multiple participants). Must include your own round 1 public package', multiple: true, }), } @@ -39,50 +39,46 @@ export class DkgRound2Command extends IronfishCommand { secretName = await selectSecret(client) } - let encryptedSecretPackage = flags.encryptedSecretPackage - if (!encryptedSecretPackage) { - encryptedSecretPackage = await CliUx.ux.prompt( - `Enter the encrypted secret package for secret ${secretName}`, - { - required: true, - }, + let round1SecretPackage = flags.round1SecretPackage + if (!round1SecretPackage) { + round1SecretPackage = await CliUx.ux.prompt( + `Enter the round 1 secret package for secret ${secretName}`, + { required: true }, ) } - let publicPackages = flags.publicPackage - if (!publicPackages || publicPackages.length < 2) { + let round1PublicPackages = flags.round1PublicPackages + if (!round1PublicPackages || round1PublicPackages.length < 2) { const input = await longPrompt( - 'Enter public packages separated by commas, one for each participant', - { - required: true, - }, + 'Enter round 1 public packages separated by commas, one for each participant', + { required: true }, ) - publicPackages = input.split(',') + round1PublicPackages = input.split(',') - if (publicPackages.length < 2) { + if (round1PublicPackages.length < 2) { this.error( - 'Must include a public package for each participant; at least 2 participants required', + 'Must include a round 1 public package for each participant; at least 2 participants required', ) } } - publicPackages = publicPackages.map((i) => i.trim()) + round1PublicPackages = round1PublicPackages.map((i) => i.trim()) const response = await client.wallet.multisig.dkg.round2({ secretName, - encryptedSecretPackage, - publicPackages, + round1SecretPackage, + round1PublicPackages, }) - this.log('\nEncrypted Secret Package:\n') - this.log(response.content.encryptedSecretPackage) + this.log('\nRound 2 Encrypted Secret Package:\n') + this.log(response.content.round2SecretPackage) this.log() - this.log('\nPublic Package:\n') - this.log(response.content.publicPackages) + this.log('\nRound 2 Public Package:\n') + this.log(response.content.round2PublicPackage) this.log() this.log() this.log('Next step:') - this.log('Send the public package to each participant') + this.log('Send the round 2 public package to each participant') } } diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts index 6cebfebe91..8e9c0e8846 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -23,18 +23,18 @@ export class DkgRound3Command extends IronfishCommand { }), round2SecretPackage: Flags.string({ char: 'e', - description: 'The encrypted secret package created during DKG round2', + description: 'The encrypted secret package created during DKG round 2', }), round1PublicPackages: Flags.string({ char: 'p', description: - 'The public package that a participant generated during DKG round1 (may be specified multiple times for multiple participants). Must include your own round1 public package', + 'The public package that a participant generated during DKG round 1 (may be specified multiple times for multiple participants). Must include your own round 1 public package', multiple: true, }), round2PublicPackages: Flags.string({ char: 'q', description: - 'The public package that a participant generated during DKG round2 where the recipient matches the identity associated with the secret', + 'The public package that a participant generated during DKG round 2 (may be specified multiple times for multiple participants). Your own round 2 public package is optional; if included, it will be ignored', multiple: true, }), } @@ -52,7 +52,7 @@ export class DkgRound3Command extends IronfishCommand { let round2SecretPackage = flags.round2SecretPackage if (!round2SecretPackage) { round2SecretPackage = await CliUx.ux.prompt( - `Enter the encrypted secret package for secret ${secretName}`, + `Enter the encrypted round 2 secret package for secret ${secretName}`, { required: true, }, @@ -71,7 +71,7 @@ export class DkgRound3Command extends IronfishCommand { if (round1PublicPackages.length < 2) { this.error( - 'Must include a public package for each participant; at least 2 participants required', + 'Must include a round 1 public package for each participant; at least 2 participants required', ) } } diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index 42ce9f89b0..8fda5d4e5b 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -250,13 +250,13 @@ export namespace multisig { } export function dkgRound1(selfIdentity: string, minSigners: number, participantIdentities: Array): DkgRound1Packages export interface DkgRound1Packages { - encryptedSecretPackage: string - publicPackage: string + round1SecretPackage: string + round1PublicPackage: string } - export function dkgRound2(secret: string, encryptedSecretPackage: string, publicPackages: Array): DkgRound2Packages + export function dkgRound2(secret: string, round1SecretPackage: string, round1PublicPackages: Array): DkgRound2Packages export interface DkgRound2Packages { - encryptedSecretPackage: string - publicPackages: string + round2SecretPackage: string + round2PublicPackage: string } export function dkgRound3(secret: ParticipantSecret, round2SecretPackage: string, round1PublicPackages: Array, round2PublicPackages: Array): DkgRound3Packages export interface DkgRound3Packages { diff --git a/ironfish-rust-nodejs/src/multisig.rs b/ironfish-rust-nodejs/src/multisig.rs index 5e30384914..957676b23a 100644 --- a/ironfish-rust-nodejs/src/multisig.rs +++ b/ironfish-rust-nodejs/src/multisig.rs @@ -377,7 +377,7 @@ pub fn dkg_round1( Identity::deserialize_from(&hex_to_vec_bytes(&self_identity).map_err(to_napi_err)?[..])?; let participant_identities = try_deserialize_identities(participant_identities)?; - let (encrypted_secret_package, public_package) = dkg::round1::round1( + let (round1_secret_package, round1_public_package) = dkg::round1::round1( &self_identity, min_signers, &participant_identities, @@ -386,48 +386,47 @@ pub fn dkg_round1( .map_err(to_napi_err)?; Ok(DkgRound1Packages { - encrypted_secret_package: bytes_to_hex(&encrypted_secret_package), - public_package: bytes_to_hex(&public_package.serialize()), + round1_secret_package: bytes_to_hex(&round1_secret_package), + round1_public_package: bytes_to_hex(&round1_public_package.serialize()), }) } #[napi(object, namespace = "multisig")] pub struct DkgRound1Packages { - pub encrypted_secret_package: String, - pub public_package: String, + pub round1_secret_package: String, + pub round1_public_package: String, } #[napi(namespace = "multisig")] pub fn dkg_round2( secret: String, - encrypted_secret_package: String, - public_packages: Vec, + round1_secret_package: String, + round1_public_packages: Vec, ) -> Result { let secret = Secret::deserialize_from(&hex_to_vec_bytes(&secret).map_err(to_napi_err)?[..])?; - let public_packages = try_deserialize(public_packages, |bytes| { + let round1_public_packages = try_deserialize(round1_public_packages, |bytes| { dkg::round1::PublicPackage::deserialize_from(bytes) })?; - let encrypted_secret_package = - hex_to_vec_bytes(&encrypted_secret_package).map_err(to_napi_err)?; + let round1_secret_package = hex_to_vec_bytes(&round1_secret_package).map_err(to_napi_err)?; - let (encrypted_secret_package, public_packages) = dkg::round2::round2( + let (round2_secret_package, round2_public_package) = dkg::round2::round2( &secret, - &encrypted_secret_package, - &public_packages, + &round1_secret_package, + &round1_public_packages, thread_rng(), ) .map_err(to_napi_err)?; Ok(DkgRound2Packages { - encrypted_secret_package: bytes_to_hex(&encrypted_secret_package), - public_packages: bytes_to_hex(&public_packages.serialize()), + round2_secret_package: bytes_to_hex(&round2_secret_package), + round2_public_package: bytes_to_hex(&round2_public_package.serialize()), }) } #[napi(object, namespace = "multisig")] pub struct DkgRound2Packages { - pub encrypted_secret_package: String, - pub public_packages: String, + pub round2_secret_package: String, + pub round2_public_package: String, } #[napi(object, namespace = "multisig")] diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.test.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.test.ts index 813230bd7c..569b048900 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.test.ts @@ -23,15 +23,15 @@ describe('Route multisig/dkg/round1', () => { const response = await routeTest.client.wallet.multisig.dkg.round1(request) expect(response.content).toMatchObject({ - encryptedSecretPackage: expect.any(String), - publicPackage: expect.any(String), + round1SecretPackage: expect.any(String), + round1PublicPackage: expect.any(String), }) - // Ensure that the encrypted secret package can be decrypted + // Ensure that the round 1 secret package can be decrypted const secretValue = await routeTest.node.wallet.walletDb.getMultisigSecretByName(secretName) Assert.isNotUndefined(secretValue) const secret = new multisig.ParticipantSecret(secretValue.secret) - secret.decryptData(Buffer.from(response.content.encryptedSecretPackage, 'hex')) + secret.decryptData(Buffer.from(response.content.round1SecretPackage, 'hex')) }) it('should fail if the named secret does not exist', async () => { @@ -71,8 +71,8 @@ describe('Route multisig/dkg/round1', () => { const response = await routeTest.client.wallet.multisig.dkg.round1(request) expect(response.content).toMatchObject({ - encryptedSecretPackage: expect.any(String), - publicPackage: expect.any(String), + round1SecretPackage: expect.any(String), + round1PublicPackage: expect.any(String), }) }) diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.ts index 66502eebea..bc711c8266 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.ts @@ -15,8 +15,8 @@ export type DkgRound1Request = { } export type DkgRound1Response = { - encryptedSecretPackage: string - publicPackage: string + round1SecretPackage: string + round1PublicPackage: string } export const DkgRound1RequestSchema: yup.ObjectSchema = yup @@ -32,8 +32,8 @@ export const DkgRound1RequestSchema: yup.ObjectSchema = yup export const DkgRound1ResponseSchema: yup.ObjectSchema = yup .object({ - encryptedSecretPackage: yup.string().defined(), - publicPackage: yup.string().defined(), + round1SecretPackage: yup.string().defined(), + round1PublicPackage: yup.string().defined(), }) .defined() diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.test.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.test.ts index 05cf72cd63..66e338dca1 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.test.ts @@ -28,25 +28,25 @@ describe('Route multisig/dkg/round2', () => { const round2Request = { secretName: secretName1, - encryptedSecretPackage: round1Response1.content.encryptedSecretPackage, - publicPackages: [ - round1Response1.content.publicPackage, - round1Response2.content.publicPackage, + round1SecretPackage: round1Response1.content.round1SecretPackage, + round1PublicPackages: [ + round1Response1.content.round1PublicPackage, + round1Response2.content.round1PublicPackage, ], } const round2Response = await routeTest.client.wallet.multisig.dkg.round2(round2Request) expect(round2Response.content).toMatchObject({ - encryptedSecretPackage: expect.any(String), + round2SecretPackage: expect.any(String), }) }) it('should fail if the named secret does not exist', async () => { const request = { secretName: 'fakeName', - encryptedSecretPackage: 'foo', - publicPackages: ['bar', 'baz'], + round1SecretPackage: 'foo', + round1PublicPackages: ['bar', 'baz'], } await expect(routeTest.client.wallet.multisig.dkg.round2(request)).rejects.toThrow( diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.ts index 5652ffb395..e979abc85d 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.ts @@ -10,27 +10,27 @@ import { AssertHasRpcContext } from '../../../rpcContext' export type DkgRound2Request = { secretName: string - encryptedSecretPackage: string - publicPackages: Array + round1SecretPackage: string + round1PublicPackages: Array } export type DkgRound2Response = { - encryptedSecretPackage: string - publicPackages: string + round2SecretPackage: string + round2PublicPackage: string } export const DkgRound2RequestSchema: yup.ObjectSchema = yup .object({ secretName: yup.string().defined(), - encryptedSecretPackage: yup.string().defined(), - publicPackages: yup.array().of(yup.string().defined()).defined(), + round1SecretPackage: yup.string().defined(), + round1PublicPackages: yup.array().of(yup.string().defined()).defined(), }) .defined() export const DkgRound2ResponseSchema: yup.ObjectSchema = yup .object({ - encryptedSecretPackage: yup.string().defined(), - publicPackages: yup.string().defined(), + round2SecretPackage: yup.string().defined(), + round2PublicPackage: yup.string().defined(), }) .defined() @@ -40,7 +40,7 @@ routes.register( async (request, node): Promise => { AssertHasRpcContext(request, node, 'wallet') - const { secretName, encryptedSecretPackage, publicPackages } = request.data + const { secretName, round1SecretPackage, round1PublicPackages } = request.data const multisigSecret = await node.wallet.walletDb.getMultisigSecretByName(secretName) if (!multisigSecret) { @@ -53,7 +53,7 @@ routes.register( const secret = multisigSecret.secret.toString('hex') - const packages = multisig.dkgRound2(secret, encryptedSecretPackage, publicPackages) + const packages = multisig.dkgRound2(secret, round1SecretPackage, round1PublicPackages) request.end(packages) }, diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts index 5004060802..2656292a8a 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts @@ -44,8 +44,8 @@ describe('Route multisig/dkg/round3', () => { secretNames.map((secretName, index) => routeTest.client.wallet.multisig.dkg.round2({ secretName, - encryptedSecretPackage: round1Packages[index].content.encryptedSecretPackage, - publicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), + round1SecretPackage: round1Packages[index].content.round1SecretPackage, + round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), }), ), ) @@ -56,9 +56,9 @@ describe('Route multisig/dkg/round3', () => { routeTest.client.wallet.multisig.dkg.round3({ secretName, accountName: accountNames[index], - round2SecretPackage: round2Packages[index].content.encryptedSecretPackage, - round1PublicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), - round2PublicPackages: round2Packages.map((pkg) => pkg.content.publicPackages), + round2SecretPackage: round2Packages[index].content.round2SecretPackage, + round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), + round2PublicPackages: round2Packages.map((pkg) => pkg.content.round2PublicPackage), }), ), ) @@ -127,8 +127,8 @@ describe('Route multisig/dkg/round3', () => { secretNames.map((secretName, index) => routeTest.client.wallet.multisig.dkg.round2({ secretName, - encryptedSecretPackage: round1Packages[index].content.encryptedSecretPackage, - publicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), + round1SecretPackage: round1Packages[index].content.round1SecretPackage, + round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), }), ), ) @@ -137,11 +137,11 @@ describe('Route multisig/dkg/round3', () => { await expect( routeTest.client.wallet.multisig.dkg.round3({ secretName: secretNames[0], - round2SecretPackage: round2Packages[0].content.encryptedSecretPackage, + round2SecretPackage: round2Packages[0].content.round2SecretPackage, round1PublicPackages: removeOneElement( - round1Packages.map((pkg) => pkg.content.publicPackage), + round1Packages.map((pkg) => pkg.content.round1PublicPackage), ), - round2PublicPackages: round2Packages.map((pkg) => pkg.content.publicPackages), + round2PublicPackages: round2Packages.map((pkg) => pkg.content.round2PublicPackage), }), ).rejects.toThrow('invalid input: expected 3 round 1 public packages, got 2') }) @@ -175,8 +175,8 @@ describe('Route multisig/dkg/round3', () => { secretNames.map((secretName, index) => routeTest.client.wallet.multisig.dkg.round2({ secretName, - encryptedSecretPackage: round1Packages[index].content.encryptedSecretPackage, - publicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), + round1SecretPackage: round1Packages[index].content.round1SecretPackage, + round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), }), ), ) @@ -185,14 +185,14 @@ describe('Route multisig/dkg/round3', () => { await expect( routeTest.client.wallet.multisig.dkg.round3({ secretName: secretNames[0], - round2SecretPackage: round2Packages[0].content.encryptedSecretPackage, - round1PublicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), + round2SecretPackage: round2Packages[0].content.round2SecretPackage, + round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), // Here we cannot just remove any one element to perform this test, // because `round2Packages[0]` does not contain any useful // information for `secretName[0]`, hence if that gets removed, the // operation won't fail. This is why we call `slice()` round2PublicPackages: removeOneElement( - round2Packages.slice(1).map((pkg) => pkg.content.publicPackages), + round2Packages.slice(1).map((pkg) => pkg.content.round2PublicPackage), ), }), ).rejects.toThrow('invalid input: expected 2 round 2 public packages, got 1') @@ -227,8 +227,8 @@ describe('Route multisig/dkg/round3', () => { secretNames.map((secretName, index) => routeTest.client.wallet.multisig.dkg.round2({ secretName, - encryptedSecretPackage: round1Packages[index].content.encryptedSecretPackage, - publicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), + round1SecretPackage: round1Packages[index].content.round1SecretPackage, + round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), }), ), ) @@ -237,9 +237,9 @@ describe('Route multisig/dkg/round3', () => { await expect( routeTest.client.wallet.multisig.dkg.round3({ secretName: secretNames[0], - round2SecretPackage: round2Packages[1].content.encryptedSecretPackage, - round1PublicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), - round2PublicPackages: round2Packages.map((pkg) => pkg.content.publicPackages), + round2SecretPackage: round2Packages[1].content.round2SecretPackage, + round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), + round2PublicPackages: round2Packages.map((pkg) => pkg.content.round2PublicPackage), }), ).rejects.toThrow('decryption error: aead::Error') }) diff --git a/ironfish/src/rpc/routes/wallet/multisig/integration.test.slow.ts b/ironfish/src/rpc/routes/wallet/multisig/integration.test.slow.ts index 391c176918..b5e3e33a48 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/integration.test.slow.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/integration.test.slow.ts @@ -192,8 +192,8 @@ describe('multisig RPC integration', () => { participants.map(({ name }, index) => routeTest.client.wallet.multisig.dkg.round2({ secretName: name, - encryptedSecretPackage: round1Packages[index].content.encryptedSecretPackage, - publicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), + round1SecretPackage: round1Packages[index].content.round1SecretPackage, + round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), }), ), ) @@ -203,9 +203,9 @@ describe('multisig RPC integration', () => { participants.map(async ({ name }, index) => { await routeTest.client.wallet.multisig.dkg.round3({ secretName: name, - round2SecretPackage: round2Packages[index].content.encryptedSecretPackage, - round1PublicPackages: round1Packages.map((pkg) => pkg.content.publicPackage), - round2PublicPackages: round2Packages.map((pkg) => pkg.content.publicPackages), + round2SecretPackage: round2Packages[index].content.round2SecretPackage, + round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), + round2PublicPackages: round2Packages.map((pkg) => pkg.content.round2PublicPackage), }) const participantAccount = routeTest.wallet.getAccountByName(name) From 87dd9a45eae59ad43118735dd84e99bb34c70e77 Mon Sep 17 00:00:00 2001 From: Daniel Cogan Date: Tue, 23 Apr 2024 16:26:02 -0400 Subject: [PATCH 27/35] Remove unused renderIron function (#4925) --- ironfish/src/utils/currency.test.ts | 9 --------- ironfish/src/utils/currency.ts | 25 ------------------------- 2 files changed, 34 deletions(-) diff --git a/ironfish/src/utils/currency.test.ts b/ironfish/src/utils/currency.test.ts index e4b8772046..a0cc7b9582 100644 --- a/ironfish/src/utils/currency.test.ts +++ b/ironfish/src/utils/currency.test.ts @@ -154,15 +154,6 @@ describe('CurrencyUtils', () => { }) }) - it('renderIron', () => { - expect(CurrencyUtils.renderIron(0n)).toEqual('0.00000000') - expect(CurrencyUtils.renderIron(1n)).toEqual('0.00000001') - expect(CurrencyUtils.renderIron(100n)).toEqual('0.00000100') - expect(CurrencyUtils.renderIron(10000n)).toEqual('0.00010000') - expect(CurrencyUtils.renderIron(100000000n)).toEqual('1.00000000') - expect(CurrencyUtils.renderIron(1n, true)).toEqual('$IRON 0.00000001') - }) - it('renderOre', () => { expect(CurrencyUtils.renderOre(0n)).toEqual('0') expect(CurrencyUtils.renderOre(1n)).toEqual('1') diff --git a/ironfish/src/utils/currency.ts b/ironfish/src/utils/currency.ts index 33dfcc66c2..a018ac7826 100644 --- a/ironfish/src/utils/currency.ts +++ b/ironfish/src/utils/currency.ts @@ -9,8 +9,6 @@ import { ErrorUtils } from './error' import { FixedNumberUtils } from './fixedNumber' export class CurrencyUtils { - static locale?: string - /** * Serializes ore as iron with up to 8 decimal places */ @@ -106,27 +104,6 @@ export class CurrencyUtils { return majorDenominationAmount } - /* - * Renders ore as iron for human-readable purposes - */ - static renderIron(amount: bigint | string, includeTicker = false, assetId?: string): string { - if (typeof amount === 'string') { - amount = this.decode(amount) - } - - const iron = FixedNumberUtils.render(amount, 8) - - if (includeTicker) { - let ticker = '$IRON' - if (assetId && !isNativeIdentifier(assetId)) { - ticker = assetId - } - return `${ticker} ${iron}` - } - - return iron - } - /* * Renders ore for human-readable purposes */ @@ -168,8 +145,6 @@ const IRON_SYMBOL = '$IRON' export const ORE_TO_IRON = 100000000 export const MINIMUM_ORE_AMOUNT = 0n export const MAXIMUM_ORE_AMOUNT = 2n ** 64n -export const MINIMUM_IRON_AMOUNT = CurrencyUtils.renderIron(MINIMUM_ORE_AMOUNT) -export const MAXIMUM_IRON_AMOUNT = CurrencyUtils.renderIron(MAXIMUM_ORE_AMOUNT) export function assetMetadataWithDefaults( assetId?: string, From c65d64c5f944c56e2151887a144b4ecbdbfd188a Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Tue, 23 Apr 2024 13:29:57 -0700 Subject: [PATCH 28/35] renames wallet:multisig:participants to wallet:multisig:account:participants (#4926) * renames wallet:multisig:participants to wallet:multisig:account:participants renames the command to make it clearer that the command will list all of the participant identities in the group for a given account and not all participant identities that the wallet has created maintains the old command name as a deprecated alias * removes alias --- .../{participants/index.ts => account/participants.ts} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename ironfish-cli/src/commands/wallet/multisig/{participants/index.ts => account/participants.ts} (84%) diff --git a/ironfish-cli/src/commands/wallet/multisig/participants/index.ts b/ironfish-cli/src/commands/wallet/multisig/account/participants.ts similarity index 84% rename from ironfish-cli/src/commands/wallet/multisig/participants/index.ts rename to ironfish-cli/src/commands/wallet/multisig/account/participants.ts index 7f6af9cfb9..80e11ef213 100644 --- a/ironfish-cli/src/commands/wallet/multisig/participants/index.ts +++ b/ironfish-cli/src/commands/wallet/multisig/account/participants.ts @@ -6,13 +6,13 @@ import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' export class MultisigAccountParticipants extends IronfishCommand { - static description = `List the participant identities for a multisig account` + static description = `List all participant identities in the group for a multisig account` static flags = { ...RemoteFlags, account: Flags.string({ char: 'f', - description: 'The account to list identities for', + description: 'The account to list group identities for', }), } From 9d923a793f793375f708bcadc6418d181241b36a Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Tue, 23 Apr 2024 17:27:01 -0400 Subject: [PATCH 29/35] feat(cli): Add CLI command to list identities (#4928) --- .../wallet/multisig/participants/index.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 ironfish-cli/src/commands/wallet/multisig/participants/index.ts diff --git a/ironfish-cli/src/commands/wallet/multisig/participants/index.ts b/ironfish-cli/src/commands/wallet/multisig/participants/index.ts new file mode 100644 index 0000000000..95df0936ef --- /dev/null +++ b/ironfish-cli/src/commands/wallet/multisig/participants/index.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 { CliUx } from '@oclif/core' +import { IronfishCommand } from '../../../../command' +import { RemoteFlags } from '../../../../flags' + +export class MultisigParticipants extends IronfishCommand { + static description = 'List out all the participant names and identities' + + static flags = { + ...RemoteFlags, + } + + async start(): Promise { + const client = await this.sdk.connectRpc() + const response = await client.wallet.multisig.getIdentities() + + const participants = [] + for (const { name, identity } of response.content.identities) { + participants.push({ + name, + value: identity, + }) + } + + // sort identities by name + participants.sort((a, b) => a.name.localeCompare(b.name)) + + CliUx.ux.table( + participants, + { + name: { + header: 'Participant Name', + get: (p) => p.name, + }, + identity: { + header: 'Identity', + get: (p) => p.value, + }, + }, + { + 'no-truncate': true, + }, + ) + } +} From 4761f24e4299e32f36e50aaec189e77ca78e78db Mon Sep 17 00:00:00 2001 From: Daniel Cogan Date: Tue, 23 Apr 2024 17:46:10 -0400 Subject: [PATCH 30/35] Use ironfish/custom decimal parsing function (#4929) --- ironfish-cli/src/commands/wallet/burn.ts | 2 +- ironfish-cli/src/commands/wallet/mint.ts | 2 +- ironfish-cli/src/commands/wallet/send.ts | 2 +- ironfish-cli/src/utils/currency.ts | 2 +- ironfish/src/utils/currency.test.ts | 4 +-- ironfish/src/utils/currency.ts | 33 ++++++++++-------------- ironfish/src/utils/fixedNumber.ts | 2 +- 7 files changed, 20 insertions(+), 27 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/burn.ts b/ironfish-cli/src/commands/wallet/burn.ts index 5dc00e2a1e..139f97c4fd 100644 --- a/ironfish-cli/src/commands/wallet/burn.ts +++ b/ironfish-cli/src/commands/wallet/burn.ts @@ -149,7 +149,7 @@ export class Burn extends IronfishCommand { ) if (error) { - this.error(`${error.reason}`) + this.error(`${error.message}`) } amount = parsedAmount diff --git a/ironfish-cli/src/commands/wallet/mint.ts b/ironfish-cli/src/commands/wallet/mint.ts index ea283d588c..5ce07c67d4 100644 --- a/ironfish-cli/src/commands/wallet/mint.ts +++ b/ironfish-cli/src/commands/wallet/mint.ts @@ -213,7 +213,7 @@ export class Mint extends IronfishCommand { ) if (error) { - this.error(`${error.reason}`) + this.error(`${error.message}`) } amount = parsedAmount diff --git a/ironfish-cli/src/commands/wallet/send.ts b/ironfish-cli/src/commands/wallet/send.ts index 0dcd546639..0e18f6a431 100644 --- a/ironfish-cli/src/commands/wallet/send.ts +++ b/ironfish-cli/src/commands/wallet/send.ts @@ -163,7 +163,7 @@ export class Send extends IronfishCommand { ) if (error) { - this.error(`${error.reason}`) + this.error(`${error.message}`) } amount = parsedAmount diff --git a/ironfish-cli/src/utils/currency.ts b/ironfish-cli/src/utils/currency.ts index a769f9ae65..960dc9f6e7 100644 --- a/ironfish-cli/src/utils/currency.ts +++ b/ironfish-cli/src/utils/currency.ts @@ -72,7 +72,7 @@ export async function promptCurrency(options: { ) if (error) { - options.logger.error(`Error: ${error.reason}`) + options.logger.error(`Error: ${error.message}`) continue } diff --git a/ironfish/src/utils/currency.test.ts b/ironfish/src/utils/currency.test.ts index a0cc7b9582..b83d41c3c3 100644 --- a/ironfish/src/utils/currency.test.ts +++ b/ironfish/src/utils/currency.test.ts @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { Asset } from '@ironfish/rust-nodejs' -import { CurrencyUtils, isParseFixedError } from './currency' +import { CurrencyUtils } from './currency' describe('CurrencyUtils', () => { it('encode', () => { @@ -94,7 +94,7 @@ describe('CurrencyUtils', () => { it('should return an error if the given amount cannot be parsed', () => { const [value, err] = CurrencyUtils.tryMajorToMinor('1.0.0') expect(value).toBeNull() - expect(isParseFixedError(err)).toEqual(true) + expect(err?.message).toEqual('too many decimal points') }) }) diff --git a/ironfish/src/utils/currency.ts b/ironfish/src/utils/currency.ts index a018ac7826..2224042a6b 100644 --- a/ironfish/src/utils/currency.ts +++ b/ironfish/src/utils/currency.ts @@ -5,7 +5,6 @@ import { formatFixed, parseFixed } from '@ethersproject/bignumber' import { isNativeIdentifier } from './asset' import { BigIntUtils } from './bigint' -import { ErrorUtils } from './error' import { FixedNumberUtils } from './fixedNumber' export class CurrencyUtils { @@ -60,13 +59,21 @@ export class CurrencyUtils { verifiedAssetMetadata?: { decimals?: number }, - ): [bigint, null] | [null, ParseFixedError] { + ): [bigint, null] | [null, Error] { + const { decimals } = assetMetadataWithDefaults(assetId, verifiedAssetMetadata) try { - const { decimals } = assetMetadataWithDefaults(assetId, verifiedAssetMetadata) - const parsed = parseFixed(amount.toString(), decimals).toBigInt() - return [parsed, null] + const { value, decimals: parsedDecimals } = FixedNumberUtils.tryDecodeDecimal( + amount.toString(), + ) + + if (parsedDecimals > decimals) { + return [null, new Error('major value is too small')] + } + + const minorValue = value * 10n ** BigInt(decimals - parsedDecimals) + return [minorValue, null] } catch (e) { - if (isParseFixedError(e)) { + if (e instanceof Error) { return [null, e] } throw e @@ -126,20 +133,6 @@ export class CurrencyUtils { } } -export interface ParseFixedError extends Error { - code: 'INVALID_ARGUMENT' | 'NUMERIC_FAULT' - reason: string -} - -export function isParseFixedError(error: unknown): error is ParseFixedError { - return ( - ErrorUtils.isNodeError(error) && - (error['code'] === 'INVALID_ARGUMENT' || error['code'] === 'NUMERIC_FAULT') && - 'reason' in error && - typeof error['reason'] === 'string' - ) -} - const IRON_DECIMAL_PLACES = 8 const IRON_SYMBOL = '$IRON' export const ORE_TO_IRON = 100000000 diff --git a/ironfish/src/utils/fixedNumber.ts b/ironfish/src/utils/fixedNumber.ts index 0f19252866..776effae96 100644 --- a/ironfish/src/utils/fixedNumber.ts +++ b/ironfish/src/utils/fixedNumber.ts @@ -43,7 +43,7 @@ export class FixedNumberUtils { const split = input.split('.') if (split.length > 2) { - throw new Error('Invalid number of decimals') + throw new Error('too many decimal points') } else if (split.length === 1) { return { value: BigInt(split[0]), decimals: 0 } } else { From 0e36ea0a4d71b4466ca1d7caf74ba33a7ef3e2d5 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:00:57 -0700 Subject: [PATCH 31/35] renames secretName to participantName (#4927) * renames secretName to participantName the term 'secretName' does not make it clear that the name should be the same as the one used with 'participant:create', that is, the name of a participant renames the flag and rpc parameter 'secretName' to 'participantName' across all dkg cli commands and rpc endpoints changes flag character from s to n. in round3, this also requires changing the 'accountName' flag character from n to a adds an alias 'name' for 'participantName' in all dkg commands * fixes lint * fixes round1 identity help text * fixes rebase errors * fixes stray usage of secretName in RPC validation --- .../commands/wallet/multisig/dkg/round1.ts | 15 ++-- .../commands/wallet/multisig/dkg/round2.ts | 15 ++-- .../commands/wallet/multisig/dkg/round3.ts | 17 +++-- .../routes/wallet/multisig/dkg/round1.test.ts | 56 ++++++++------ .../rpc/routes/wallet/multisig/dkg/round1.ts | 10 +-- .../routes/wallet/multisig/dkg/round2.test.ts | 20 ++--- .../rpc/routes/wallet/multisig/dkg/round2.ts | 10 +-- .../routes/wallet/multisig/dkg/round3.test.ts | 76 ++++++++++--------- .../rpc/routes/wallet/multisig/dkg/round3.ts | 12 +-- .../wallet/multisig/integration.test.slow.ts | 10 +-- 10 files changed, 129 insertions(+), 112 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts index dfc4f012e8..b38019b6ec 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts @@ -13,14 +13,15 @@ export class DkgRound1Command extends IronfishCommand { static flags = { ...RemoteFlags, - secretName: Flags.string({ - char: 's', + participantName: Flags.string({ + char: 'n', description: 'The name of the secret to use for encryption during DKG', + aliases: ['name'], }), identity: Flags.string({ char: 'i', description: - 'The identity of the participants will generate the group keys (may be specified multiple times to add multiple participants). Must include the identity for secretName', + 'The identity of the participants will generate the group keys (may be specified multiple times to add multiple participants)', multiple: true, }), minSigners: Flags.integer({ @@ -34,9 +35,9 @@ export class DkgRound1Command extends IronfishCommand { const client = await this.sdk.connectRpc() - let secretName = flags.secretName - if (!secretName) { - secretName = await selectSecret(client) + let participantName = flags.participantName + if (!participantName) { + participantName = await selectSecret(client) } let identities = flags.identity @@ -64,7 +65,7 @@ export class DkgRound1Command extends IronfishCommand { } const response = await client.wallet.multisig.dkg.round1({ - secretName: secretName, + participantName, participants: identities.map((identity) => ({ identity })), minSigners: minSigners, }) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts index 2f1725ecd2..709588a34b 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts @@ -13,9 +13,10 @@ export class DkgRound2Command extends IronfishCommand { static flags = { ...RemoteFlags, - secretName: Flags.string({ - char: 's', + participantName: Flags.string({ + char: 'n', description: 'The name of the secret to use for encryption during DKG', + aliases: ['name'], }), round1SecretPackage: Flags.string({ char: 'e', @@ -34,15 +35,15 @@ export class DkgRound2Command extends IronfishCommand { const client = await this.sdk.connectRpc() - let secretName = flags.secretName - if (!secretName) { - secretName = await selectSecret(client) + let participantName = flags.participantName + if (!participantName) { + participantName = await selectSecret(client) } let round1SecretPackage = flags.round1SecretPackage if (!round1SecretPackage) { round1SecretPackage = await CliUx.ux.prompt( - `Enter the round 1 secret package for secret ${secretName}`, + `Enter the round 1 secret package for participant ${participantName}`, { required: true }, ) } @@ -64,7 +65,7 @@ export class DkgRound2Command extends IronfishCommand { round1PublicPackages = round1PublicPackages.map((i) => i.trim()) const response = await client.wallet.multisig.dkg.round2({ - secretName, + participantName, round1SecretPackage, round1PublicPackages, }) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts index 8e9c0e8846..fc1c40ff7d 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -13,12 +13,13 @@ export class DkgRound3Command extends IronfishCommand { static flags = { ...RemoteFlags, - secretName: Flags.string({ - char: 's', + participantName: Flags.string({ + char: 'n', description: 'The name of the secret to use for decryption during DKG', + aliases: ['name'], }), accountName: Flags.string({ - char: 'n', + char: 'a', description: 'The name to set for the imported account', }), round2SecretPackage: Flags.string({ @@ -44,15 +45,15 @@ export class DkgRound3Command extends IronfishCommand { const client = await this.sdk.connectRpc() - let secretName = flags.secretName - if (!secretName) { - secretName = await selectSecret(client) + let participantName = flags.participantName + if (!participantName) { + participantName = await selectSecret(client) } let round2SecretPackage = flags.round2SecretPackage if (!round2SecretPackage) { round2SecretPackage = await CliUx.ux.prompt( - `Enter the encrypted round 2 secret package for secret ${secretName}`, + `Enter the encrypted secret package for participant ${participantName}`, { required: true, }, @@ -103,7 +104,7 @@ export class DkgRound3Command extends IronfishCommand { round2PublicPackages = round2PublicPackages.map((i) => i.trim()) const response = await client.wallet.multisig.dkg.round3({ - secretName: secretName, + participantName, accountName: flags.accountName, round2SecretPackage, round1PublicPackages, diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.test.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.test.ts index 569b048900..552812283f 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.test.ts @@ -9,17 +9,18 @@ describe('Route multisig/dkg/round1', () => { const routeTest = createRouteTest() it('should create round 1 packages', async () => { - const secretName = 'name' - await routeTest.client.wallet.multisig.createParticipant({ name: secretName }) + const participantName = 'name' + await routeTest.client.wallet.multisig.createParticipant({ name: participantName }) - const identity = (await routeTest.client.wallet.multisig.getIdentity({ name: secretName })) - .content.identity + const identity = ( + await routeTest.client.wallet.multisig.getIdentity({ name: participantName }) + ).content.identity const otherParticipants = Array.from({ length: 2 }, () => ({ identity: multisig.ParticipantSecret.random().toIdentity().serialize().toString('hex'), })) const participants = [{ identity }, ...otherParticipants] - const request = { secretName, minSigners: 2, participants } + const request = { participantName, minSigners: 2, participants } const response = await routeTest.client.wallet.multisig.dkg.round1(request) expect(response.content).toMatchObject({ @@ -27,25 +28,28 @@ describe('Route multisig/dkg/round1', () => { round1PublicPackage: expect.any(String), }) - // Ensure that the round 1 secret package can be decrypted - const secretValue = await routeTest.node.wallet.walletDb.getMultisigSecretByName(secretName) + // Ensure that the encrypted secret package can be decrypted + const secretValue = await routeTest.node.wallet.walletDb.getMultisigSecretByName( + participantName, + ) Assert.isNotUndefined(secretValue) const secret = new multisig.ParticipantSecret(secretValue.secret) secret.decryptData(Buffer.from(response.content.round1SecretPackage, 'hex')) }) it('should fail if the named secret does not exist', async () => { - const secretName = 'name' - await routeTest.client.wallet.multisig.createParticipant({ name: secretName }) + const participantName = 'name' + await routeTest.client.wallet.multisig.createParticipant({ name: participantName }) - const identity = (await routeTest.client.wallet.multisig.getIdentity({ name: secretName })) - .content.identity + const identity = ( + await routeTest.client.wallet.multisig.getIdentity({ name: participantName }) + ).content.identity const otherParticipants = Array.from({ length: 2 }, () => ({ identity: multisig.ParticipantSecret.random().toIdentity().serialize().toString('hex'), })) const participants = [{ identity }, ...otherParticipants] - const request = { secretName: 'otherName', minSigners: 2, participants } + const request = { participantName: 'otherName', minSigners: 2, participants } await expect(routeTest.client.wallet.multisig.dkg.round1(request)).rejects.toThrow( expect.objectContaining({ @@ -56,8 +60,8 @@ describe('Route multisig/dkg/round1', () => { }) it('should add the named identity if it is not in the list of participants', async () => { - const secretName = 'name' - await routeTest.client.wallet.multisig.createParticipant({ name: secretName }) + const participantName = 'name' + await routeTest.client.wallet.multisig.createParticipant({ name: participantName }) // only pass in one participant const participants = [ @@ -66,7 +70,7 @@ describe('Route multisig/dkg/round1', () => { }, ] - const request = { secretName, minSigners: 2, participants } + const request = { participantName, minSigners: 2, participants } const response = await routeTest.client.wallet.multisig.dkg.round1(request) @@ -77,17 +81,18 @@ describe('Route multisig/dkg/round1', () => { }) it('should fail if minSigners is too low', async () => { - const secretName = 'name' - await routeTest.client.wallet.multisig.createParticipant({ name: secretName }) + const participantName = 'name' + await routeTest.client.wallet.multisig.createParticipant({ name: participantName }) - const identity = (await routeTest.client.wallet.multisig.getIdentity({ name: secretName })) - .content.identity + const identity = ( + await routeTest.client.wallet.multisig.getIdentity({ name: participantName }) + ).content.identity const otherParticipants = Array.from({ length: 2 }, () => ({ identity: multisig.ParticipantSecret.random().toIdentity().serialize().toString('hex'), })) const participants = [{ identity }, ...otherParticipants] - const request = { secretName, minSigners: 1, participants } + const request = { participantName, minSigners: 1, participants } await expect(routeTest.client.wallet.multisig.dkg.round1(request)).rejects.toThrow( expect.objectContaining({ @@ -98,17 +103,18 @@ describe('Route multisig/dkg/round1', () => { }) it('should fail if minSigners exceeds the number of participants', async () => { - const secretName = 'name' - await routeTest.client.wallet.multisig.createParticipant({ name: secretName }) + const participantName = 'name' + await routeTest.client.wallet.multisig.createParticipant({ name: participantName }) - const identity = (await routeTest.client.wallet.multisig.getIdentity({ name: secretName })) - .content.identity + const identity = ( + await routeTest.client.wallet.multisig.getIdentity({ name: participantName }) + ).content.identity const otherParticipants = Array.from({ length: 2 }, () => ({ identity: multisig.ParticipantSecret.random().toIdentity().serialize().toString('hex'), })) const participants = [{ identity }, ...otherParticipants] - const request = { secretName, minSigners: 4, participants } + const request = { participantName, minSigners: 4, participants } await expect(routeTest.client.wallet.multisig.dkg.round1(request)).rejects.toThrow( expect.objectContaining({ diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.ts index bc711c8266..5d599e6e4f 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.ts @@ -9,7 +9,7 @@ import { routes } from '../../../router' import { AssertHasRpcContext } from '../../../rpcContext' export type DkgRound1Request = { - secretName: string + participantName: string minSigners: number participants: Array<{ identity: string }> } @@ -21,7 +21,7 @@ export type DkgRound1Response = { export const DkgRound1RequestSchema: yup.ObjectSchema = yup .object({ - secretName: yup.string().defined(), + participantName: yup.string().defined(), minSigners: yup.number().defined(), participants: yup .array() @@ -43,12 +43,12 @@ routes.register( async (request, node): Promise => { AssertHasRpcContext(request, node, 'wallet') - const { secretName, minSigners, participants } = request.data - const multisigSecret = await node.wallet.walletDb.getMultisigSecretByName(secretName) + const { participantName, minSigners, participants } = request.data + const multisigSecret = await node.wallet.walletDb.getMultisigSecretByName(participantName) if (!multisigSecret) { throw new RpcValidationError( - `Multisig secret with name '${secretName}' not found`, + `Multisig secret with name '${participantName}' not found`, 400, RPC_ERROR_CODES.MULTISIG_SECRET_NOT_FOUND, ) diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.test.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.test.ts index 66e338dca1..bad3e632df 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.test.ts @@ -7,27 +7,27 @@ describe('Route multisig/dkg/round2', () => { const routeTest = createRouteTest() it('should create round 2 packages', async () => { - const secretName1 = 'name1' - await routeTest.client.wallet.multisig.createParticipant({ name: secretName1 }) - const secretName2 = 'name2' - await routeTest.client.wallet.multisig.createParticipant({ name: secretName2 }) + const participantName1 = 'name1' + await routeTest.client.wallet.multisig.createParticipant({ name: participantName1 }) + const participantName2 = 'name2' + await routeTest.client.wallet.multisig.createParticipant({ name: participantName2 }) const identity1 = ( - await routeTest.client.wallet.multisig.getIdentity({ name: secretName1 }) + await routeTest.client.wallet.multisig.getIdentity({ name: participantName1 }) ).content.identity const identity2 = ( - await routeTest.client.wallet.multisig.getIdentity({ name: secretName2 }) + await routeTest.client.wallet.multisig.getIdentity({ name: participantName2 }) ).content.identity const participants = [{ identity: identity1 }, { identity: identity2 }] - const round1Request1 = { secretName: secretName1, minSigners: 2, participants } + const round1Request1 = { participantName: participantName1, minSigners: 2, participants } const round1Response1 = await routeTest.client.wallet.multisig.dkg.round1(round1Request1) - const round1Request2 = { secretName: secretName2, minSigners: 2, participants } + const round1Request2 = { participantName: participantName2, minSigners: 2, participants } const round1Response2 = await routeTest.client.wallet.multisig.dkg.round1(round1Request2) const round2Request = { - secretName: secretName1, + participantName: participantName1, round1SecretPackage: round1Response1.content.round1SecretPackage, round1PublicPackages: [ round1Response1.content.round1PublicPackage, @@ -44,7 +44,7 @@ describe('Route multisig/dkg/round2', () => { it('should fail if the named secret does not exist', async () => { const request = { - secretName: 'fakeName', + participantName: 'fakeName', round1SecretPackage: 'foo', round1PublicPackages: ['bar', 'baz'], } diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.ts index e979abc85d..3d87ae5b8b 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.ts @@ -9,7 +9,7 @@ import { routes } from '../../../router' import { AssertHasRpcContext } from '../../../rpcContext' export type DkgRound2Request = { - secretName: string + participantName: string round1SecretPackage: string round1PublicPackages: Array } @@ -21,7 +21,7 @@ export type DkgRound2Response = { export const DkgRound2RequestSchema: yup.ObjectSchema = yup .object({ - secretName: yup.string().defined(), + participantName: yup.string().defined(), round1SecretPackage: yup.string().defined(), round1PublicPackages: yup.array().of(yup.string().defined()).defined(), }) @@ -40,12 +40,12 @@ routes.register( async (request, node): Promise => { AssertHasRpcContext(request, node, 'wallet') - const { secretName, round1SecretPackage, round1PublicPackages } = request.data - const multisigSecret = await node.wallet.walletDb.getMultisigSecretByName(secretName) + const { participantName, round1SecretPackage, round1PublicPackages } = request.data + const multisigSecret = await node.wallet.walletDb.getMultisigSecretByName(participantName) if (!multisigSecret) { throw new RpcValidationError( - `Multisig secret with name '${secretName}' not found`, + `Multisig secret with name '${participantName}' not found`, 400, RPC_ERROR_CODES.MULTISIG_SECRET_NOT_FOUND, ) diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts index 2656292a8a..a3e9ea1f25 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts @@ -15,24 +15,26 @@ describe('Route multisig/dkg/round3', () => { const routeTest = createRouteTest() it('should create round 3 packages', async () => { - const secretNames = ['secret-0', 'secret-1', 'secret-2'] + const participantNames = ['secret-0', 'secret-1', 'secret-2'] const accountNames = ['account-0', 'account-1', 'account-2'] // Create participants and retrieve their identities await Promise.all( - secretNames.map((name) => routeTest.client.wallet.multisig.createParticipant({ name })), + participantNames.map((name) => + routeTest.client.wallet.multisig.createParticipant({ name }), + ), ) const participants = await Promise.all( - secretNames.map( + participantNames.map( async (name) => (await routeTest.client.wallet.multisig.getIdentity({ name })).content, ), ) // Perform DKG round 1 const round1Packages = await Promise.all( - secretNames.map((secretName) => + participantNames.map((participantName) => routeTest.client.wallet.multisig.dkg.round1({ - secretName, + participantName, minSigners: 2, participants, }), @@ -41,9 +43,9 @@ describe('Route multisig/dkg/round3', () => { // Perform DKG round 2 const round2Packages = await Promise.all( - secretNames.map((secretName, index) => + participantNames.map((participantName, index) => routeTest.client.wallet.multisig.dkg.round2({ - secretName, + participantName, round1SecretPackage: round1Packages[index].content.round1SecretPackage, round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), }), @@ -52,9 +54,9 @@ describe('Route multisig/dkg/round3', () => { // Perform DKG round 3 const round3Responses = await Promise.all( - secretNames.map((secretName, index) => + participantNames.map((participantName, index) => routeTest.client.wallet.multisig.dkg.round3({ - secretName, + participantName, accountName: accountNames[index], round2SecretPackage: round2Packages[index].content.round2SecretPackage, round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), @@ -99,23 +101,25 @@ describe('Route multisig/dkg/round3', () => { }) it('should fail if not all round 1 packages are passed as an input', async () => { - const secretNames = ['secret-0', 'secret-1', 'secret-2'] + const participantNames = ['secret-0', 'secret-1', 'secret-2'] // Create participants and retrieve their identities await Promise.all( - secretNames.map((name) => routeTest.client.wallet.multisig.createParticipant({ name })), + participantNames.map((name) => + routeTest.client.wallet.multisig.createParticipant({ name }), + ), ) const participants = await Promise.all( - secretNames.map( + participantNames.map( async (name) => (await routeTest.client.wallet.multisig.getIdentity({ name })).content, ), ) // Perform DKG round 1 const round1Packages = await Promise.all( - secretNames.map((secretName) => + participantNames.map((participantName) => routeTest.client.wallet.multisig.dkg.round1({ - secretName, + participantName, minSigners: 2, participants, }), @@ -124,9 +128,9 @@ describe('Route multisig/dkg/round3', () => { // Perform DKG round 2 const round2Packages = await Promise.all( - secretNames.map((secretName, index) => + participantNames.map((participantName, index) => routeTest.client.wallet.multisig.dkg.round2({ - secretName, + participantName, round1SecretPackage: round1Packages[index].content.round1SecretPackage, round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), }), @@ -136,7 +140,7 @@ describe('Route multisig/dkg/round3', () => { // Perform DKG round 3 await expect( routeTest.client.wallet.multisig.dkg.round3({ - secretName: secretNames[0], + participantName: participantNames[0], round2SecretPackage: round2Packages[0].content.round2SecretPackage, round1PublicPackages: removeOneElement( round1Packages.map((pkg) => pkg.content.round1PublicPackage), @@ -147,23 +151,25 @@ describe('Route multisig/dkg/round3', () => { }) it('should fail if not all round 2 packages are passed as an input', async () => { - const secretNames = ['secret-0', 'secret-1', 'secret-2'] + const participantNames = ['secret-0', 'secret-1', 'secret-2'] // Create participants and retrieve their identities await Promise.all( - secretNames.map((name) => routeTest.client.wallet.multisig.createParticipant({ name })), + participantNames.map((name) => + routeTest.client.wallet.multisig.createParticipant({ name }), + ), ) const participants = await Promise.all( - secretNames.map( + participantNames.map( async (name) => (await routeTest.client.wallet.multisig.getIdentity({ name })).content, ), ) // Perform DKG round 1 const round1Packages = await Promise.all( - secretNames.map((secretName) => + participantNames.map((participantName) => routeTest.client.wallet.multisig.dkg.round1({ - secretName, + participantName, minSigners: 2, participants, }), @@ -172,9 +178,9 @@ describe('Route multisig/dkg/round3', () => { // Perform DKG round 2 const round2Packages = await Promise.all( - secretNames.map((secretName, index) => + participantNames.map((participantName, index) => routeTest.client.wallet.multisig.dkg.round2({ - secretName, + participantName, round1SecretPackage: round1Packages[index].content.round1SecretPackage, round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), }), @@ -184,12 +190,12 @@ describe('Route multisig/dkg/round3', () => { // Perform DKG round 3 await expect( routeTest.client.wallet.multisig.dkg.round3({ - secretName: secretNames[0], + participantName: participantNames[0], round2SecretPackage: round2Packages[0].content.round2SecretPackage, round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), // Here we cannot just remove any one element to perform this test, // because `round2Packages[0]` does not contain any useful - // information for `secretName[0]`, hence if that gets removed, the + // information for `participantName[0]`, hence if that gets removed, the // operation won't fail. This is why we call `slice()` round2PublicPackages: removeOneElement( round2Packages.slice(1).map((pkg) => pkg.content.round2PublicPackage), @@ -199,23 +205,25 @@ describe('Route multisig/dkg/round3', () => { }) it('should fail passing the wrong round 2 secret package', async () => { - const secretNames = ['secret-0', 'secret-1', 'secret-2'] + const participantNames = ['secret-0', 'secret-1', 'secret-2'] // Create participants and retrieve their identities await Promise.all( - secretNames.map((name) => routeTest.client.wallet.multisig.createParticipant({ name })), + participantNames.map((name) => + routeTest.client.wallet.multisig.createParticipant({ name }), + ), ) const participants = await Promise.all( - secretNames.map( + participantNames.map( async (name) => (await routeTest.client.wallet.multisig.getIdentity({ name })).content, ), ) // Perform DKG round 1 const round1Packages = await Promise.all( - secretNames.map((secretName) => + participantNames.map((participantName) => routeTest.client.wallet.multisig.dkg.round1({ - secretName, + participantName, minSigners: 2, participants, }), @@ -224,9 +232,9 @@ describe('Route multisig/dkg/round3', () => { // Perform DKG round 2 const round2Packages = await Promise.all( - secretNames.map((secretName, index) => + participantNames.map((participantName, index) => routeTest.client.wallet.multisig.dkg.round2({ - secretName, + participantName, round1SecretPackage: round1Packages[index].content.round1SecretPackage, round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), }), @@ -236,7 +244,7 @@ describe('Route multisig/dkg/round3', () => { // Perform DKG round 3 await expect( routeTest.client.wallet.multisig.dkg.round3({ - secretName: secretNames[0], + participantName: participantNames[0], round2SecretPackage: round2Packages[1].content.round2SecretPackage, round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), round2PublicPackages: round2Packages.map((pkg) => pkg.content.round2PublicPackage), diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts index e74660c615..92bdf536ae 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts @@ -11,7 +11,7 @@ import { ApiNamespace } from '../../../namespaces' import { routes } from '../../../router' export type DkgRound3Request = { - secretName: string + participantName: string round2SecretPackage: string round1PublicPackages: Array round2PublicPackages: Array @@ -26,7 +26,7 @@ export type DkgRound3Response = { export const DkgRound3RequestSchema: yup.ObjectSchema = yup .object({ - secretName: yup.string().defined(), + participantName: yup.string().defined(), round2SecretPackage: yup.string().defined(), round1PublicPackages: yup.array().of(yup.string().defined()).defined(), round2PublicPackages: yup.array().of(yup.string().defined()).defined(), @@ -48,12 +48,12 @@ routes.register( async (request, node): Promise => { Assert.isInstanceOf(node, FullNode) - const { secretName } = request.data - const multisigSecret = await node.wallet.walletDb.getMultisigSecretByName(secretName) + const { participantName } = request.data + const multisigSecret = await node.wallet.walletDb.getMultisigSecretByName(participantName) if (!multisigSecret) { throw new RpcValidationError( - `Multisig secret with name '${secretName}' not found`, + `Multisig secret with name '${participantName}' not found`, 400, RPC_ERROR_CODES.MULTISIG_SECRET_NOT_FOUND, ) @@ -78,7 +78,7 @@ routes.register( ) const accountImport = { - name: request.data.accountName ?? secretName, + name: request.data.accountName ?? participantName, version: ACCOUNT_SCHEMA_VERSION, createdAt: null, spendingKey: null, diff --git a/ironfish/src/rpc/routes/wallet/multisig/integration.test.slow.ts b/ironfish/src/rpc/routes/wallet/multisig/integration.test.slow.ts index b5e3e33a48..f6a999b32c 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/integration.test.slow.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/integration.test.slow.ts @@ -106,10 +106,10 @@ describe('multisig RPC integration', () => { } function createParticipants( - secretNames: Array, + participantNames: Array, ): Promise> { return Promise.all( - secretNames.map(async (name) => { + participantNames.map(async (name) => { const identity = (await routeTest.client.wallet.multisig.createParticipant({ name })) .content.identity return { name, identity } @@ -180,7 +180,7 @@ describe('multisig RPC integration', () => { const round1Packages = await Promise.all( participants.map(({ name }) => routeTest.client.wallet.multisig.dkg.round1({ - secretName: name, + participantName: name, minSigners, participants, }), @@ -191,7 +191,7 @@ describe('multisig RPC integration', () => { const round2Packages = await Promise.all( participants.map(({ name }, index) => routeTest.client.wallet.multisig.dkg.round2({ - secretName: name, + participantName: name, round1SecretPackage: round1Packages[index].content.round1SecretPackage, round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), }), @@ -202,7 +202,7 @@ describe('multisig RPC integration', () => { const participantAccounts = await Promise.all( participants.map(async ({ name }, index) => { await routeTest.client.wallet.multisig.dkg.round3({ - secretName: name, + participantName: name, round2SecretPackage: round2Packages[index].content.round2SecretPackage, round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), round2PublicPackages: round2Packages.map((pkg) => pkg.content.round2PublicPackage), From 5e5b55aa5e07284dab3d170de81e31b86c1af772 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:36:31 -0700 Subject: [PATCH 32/35] updates text of identities prompts (#4930) * updates text of identities prompts clarifies which participant identities are expected when prompting for a list of identities on the command line in 'commitment:create', 'dealer:create', and 'dkg:round1' * adds missing commas before 'separated by commas' --- .../src/commands/wallet/multisig/commitment/aggregate.ts | 2 +- .../src/commands/wallet/multisig/commitment/create.ts | 9 ++++++--- .../src/commands/wallet/multisig/dealer/create.ts | 9 ++++++--- ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts | 9 ++++++--- ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts | 2 +- ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts | 4 ++-- .../src/commands/wallet/multisig/signature/aggregate.ts | 2 +- 7 files changed, 23 insertions(+), 14 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts b/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts index e71f8091ce..15fc42fbbd 100644 --- a/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts +++ b/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts @@ -48,7 +48,7 @@ export class CreateSigningPackage extends IronfishCommand { let commitments = options.commitment if (!commitments) { - const input = await longPrompt('Enter the signing commitments separated by commas', { + const input = await longPrompt('Enter the signing commitments, separated by commas', { required: true, }) commitments = input.split(',') diff --git a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts index 09f29030e2..cebe1e802d 100644 --- a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts @@ -47,9 +47,12 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { let identities = options.identity if (!identities || identities.length < 2) { - const input = await longPrompt('Enter the identities separated by commas', { - required: true, - }) + const input = await longPrompt( + 'Enter the identities of all participants who will sign the transaction, separated by commas', + { + required: true, + }, + ) identities = input.split(',') if (identities.length < 2) { diff --git a/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts b/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts index 1dcbba8214..6990f829e3 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts @@ -38,9 +38,12 @@ export class MultisigCreateDealer extends IronfishCommand { let identities = flags.identity if (!identities || identities.length < 2) { - const input = await longPrompt('Enter the identities separated by commas', { - required: true, - }) + const input = await longPrompt( + 'Enter the identities of all participants, separated by commas', + { + required: true, + }, + ) identities = input.split(',') if (identities.length < 2) { diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts index b38019b6ec..34de85dd77 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts @@ -42,9 +42,12 @@ export class DkgRound1Command extends IronfishCommand { let identities = flags.identity if (!identities || identities.length < 2) { - const input = await longPrompt('Enter the identities separated by commas', { - required: true, - }) + const input = await longPrompt( + 'Enter the identities of all participants, separated by commas', + { + required: true, + }, + ) identities = input.split(',') if (identities.length < 2) { diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts index 709588a34b..283b9dfe52 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts @@ -51,7 +51,7 @@ export class DkgRound2Command extends IronfishCommand { let round1PublicPackages = flags.round1PublicPackages if (!round1PublicPackages || round1PublicPackages.length < 2) { const input = await longPrompt( - 'Enter round 1 public packages separated by commas, one for each participant', + 'Enter round 1 public packages, separated by commas, one for each participant', { required: true }, ) round1PublicPackages = input.split(',') diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts index fc1c40ff7d..68524b5c5c 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -63,7 +63,7 @@ export class DkgRound3Command extends IronfishCommand { let round1PublicPackages = flags.round1PublicPackages if (!round1PublicPackages || round1PublicPackages.length < 2) { const input = await longPrompt( - 'Enter round 1 public packages separated by commas, one for each participant', + 'Enter round 1 public packages, separated by commas, one for each participant', { required: true, }, @@ -81,7 +81,7 @@ export class DkgRound3Command extends IronfishCommand { let round2PublicPackages = flags.round2PublicPackages if (!round2PublicPackages) { const input = await longPrompt( - 'Enter round 2 public packages separated by commas, one for each participant', + 'Enter round 2 public packages, separated by commas, one for each participant', { required: true, }, diff --git a/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts b/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts index 807f52e282..d644a77a18 100644 --- a/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts +++ b/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts @@ -55,7 +55,7 @@ export class MultisigSign extends IronfishCommand { let signatureShares = options.signatureShare if (!signatureShares) { - const input = await longPrompt('Enter the signature shares separated by commas', { + const input = await longPrompt('Enter the signature shares, separated by commas', { required: true, }) signatureShares = input.split(',') From bf03a9619a26d7c325af078b615d0ad77a7b0ea3 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Thu, 25 Apr 2024 11:35:46 -0400 Subject: [PATCH 33/35] feat(cli): Unhide multisig commands (#4932) --- ironfish-cli/src/commands/wallet/multisig/dealer/create.ts | 1 - ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts | 1 - ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts | 1 - ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts | 1 - 4 files changed, 4 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts b/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts index 6990f829e3..eef5adbee7 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts @@ -9,7 +9,6 @@ import { longPrompt } from '../../../../utils/longPrompt' export class MultisigCreateDealer extends IronfishCommand { static description = `Create a set of multisig accounts from participant identities` - static hidden = true static flags = { ...RemoteFlags, diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts index 34de85dd77..5625b0ea21 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts @@ -9,7 +9,6 @@ import { selectSecret } from '../../../../utils/multisig' export class DkgRound1Command extends IronfishCommand { static description = 'Perform round1 of the DKG protocol for multisig account creation' - static hidden = true static flags = { ...RemoteFlags, diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts index 283b9dfe52..ac0fb45c2c 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts @@ -9,7 +9,6 @@ import { selectSecret } from '../../../../utils/multisig' export class DkgRound2Command extends IronfishCommand { static description = 'Perform round2 of the DKG protocol for multisig account creation' - static hidden = true static flags = { ...RemoteFlags, diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts index 68524b5c5c..f65ff4ed20 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -9,7 +9,6 @@ import { selectSecret } from '../../../../utils/multisig' export class DkgRound3Command extends IronfishCommand { static description = 'Perform round3 of the DKG protocol for multisig account creation' - static hidden = true static flags = { ...RemoteFlags, From 976c0582f01184a6e9d2bfd2ef0fe9dad535cd67 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Thu, 25 Apr 2024 09:04:50 -0700 Subject: [PATCH 34/35] adds cli command to view details for raw, unsigned transactions (#4931) allows users to view details for a transaction without needing to use the REPL useful for situations where you have a serialized transaction, but don't know what's in it --- .../src/commands/wallet/transaction/view.ts | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 ironfish-cli/src/commands/wallet/transaction/view.ts diff --git a/ironfish-cli/src/commands/wallet/transaction/view.ts b/ironfish-cli/src/commands/wallet/transaction/view.ts new file mode 100644 index 0000000000..030561055b --- /dev/null +++ b/ironfish-cli/src/commands/wallet/transaction/view.ts @@ -0,0 +1,125 @@ +/* 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 { + ErrorUtils, + RawTransaction, + RawTransactionSerde, + RpcClient, + UnsignedTransaction, +} from '@ironfish/sdk' +import { Flags } from '@oclif/core' +import inquirer from 'inquirer' +import { IronfishCommand } from '../../../command' +import { RemoteFlags } from '../../../flags' +import { longPrompt } from '../../../utils/longPrompt' +import { + renderRawTransactionDetails, + renderUnsignedTransactionDetails, +} from '../../../utils/transaction' + +export class TransactionViewCommand extends IronfishCommand { + static description = `View transaction details` + + static flags = { + ...RemoteFlags, + account: Flags.string({ + char: 'f', + description: 'The name of the account to use to for viewing transaction details', + }), + transaction: Flags.string({ + char: 't', + description: 'The hex-encoded raw transaction or unsigned transaction to view', + }), + } + + async start(): Promise { + const { flags } = await this.parse(TransactionViewCommand) + + const client = await this.sdk.connectRpc() + + const account = flags.account ?? (await this.selectAccount(client)) + + let transactionString = flags.transaction as string + if (!transactionString) { + transactionString = await longPrompt( + 'Enter the hex-encoded raw transaction or unsigned transaction to view', + { + required: true, + }, + ) + } + + const rawTransaction = this.tryDeserializeRawTransaction(transactionString) + if (rawTransaction) { + return await renderRawTransactionDetails(client, rawTransaction, account, this.logger) + } + + const unsignedTransaction = this.tryDeserializeUnsignedTransaction(transactionString) + if (unsignedTransaction) { + return await renderUnsignedTransactionDetails( + client, + unsignedTransaction, + account, + this.logger, + ) + } + + this.error( + 'Unable to deserialize transaction input as a raw transacton or an unsigned transaction', + ) + } + + async selectAccount(client: Pick): Promise { + const accountsResponse = await client.wallet.getAccounts() + + const choices = [] + for (const account of accountsResponse.content.accounts) { + choices.push({ + account, + value: account, + }) + } + + choices.sort() + + const selection = await inquirer.prompt<{ + account: string + }>([ + { + name: 'account', + message: 'Select account', + type: 'list', + choices, + }, + ]) + + return selection.account + } + + tryDeserializeRawTransaction(transaction: string): RawTransaction | undefined { + try { + return RawTransactionSerde.deserialize(Buffer.from(transaction, 'hex')) + } catch (e) { + this.logger.debug( + `Failed to deserialize transaction as RawTransaction: ${ErrorUtils.renderError(e)}`, + ) + + return undefined + } + } + + tryDeserializeUnsignedTransaction(transaction: string): UnsignedTransaction | undefined { + try { + return new UnsignedTransaction(Buffer.from(transaction, 'hex')) + } catch (e) { + this.logger.debug( + `Failed to deserialize transaction as UnsignedTransaction: ${ErrorUtils.renderError( + e, + )}`, + ) + + return undefined + } + } +} From 4e5fff0ebe6fcf4d7546afe6b23524467de22bf8 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Thu, 25 Apr 2024 14:19:59 -0400 Subject: [PATCH 35/35] v2.3.0 (#4934) --- ironfish-cli/package.json | 6 +++--- ironfish-rust-nodejs/npm/darwin-arm64/package.json | 2 +- ironfish-rust-nodejs/npm/darwin-x64/package.json | 2 +- ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json | 2 +- ironfish-rust-nodejs/npm/linux-arm64-musl/package.json | 2 +- ironfish-rust-nodejs/npm/linux-x64-gnu/package.json | 2 +- ironfish-rust-nodejs/npm/linux-x64-musl/package.json | 2 +- ironfish-rust-nodejs/npm/win32-x64-msvc/package.json | 2 +- ironfish-rust-nodejs/package.json | 2 +- ironfish/package.json | 6 +++--- 10 files changed, 14 insertions(+), 14 deletions(-) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index 9d70dfa0ec..a965f5a340 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -1,6 +1,6 @@ { "name": "ironfish", - "version": "2.2.0", + "version": "2.3.0", "description": "CLI for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js", @@ -62,8 +62,8 @@ "@aws-sdk/client-s3": "3", "@aws-sdk/client-secrets-manager": "3", "@aws-sdk/s3-request-presigner": "3", - "@ironfish/rust-nodejs": "2.2.0", - "@ironfish/sdk": "2.2.0", + "@ironfish/rust-nodejs": "2.3.0", + "@ironfish/sdk": "2.3.0", "@oclif/core": "1.23.1", "@oclif/plugin-help": "5.1.12", "@oclif/plugin-not-found": "2.3.1", diff --git a/ironfish-rust-nodejs/npm/darwin-arm64/package.json b/ironfish-rust-nodejs/npm/darwin-arm64/package.json index d59da048e9..00965059f0 100644 --- a/ironfish-rust-nodejs/npm/darwin-arm64/package.json +++ b/ironfish-rust-nodejs/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-darwin-arm64", - "version": "2.2.0", + "version": "2.3.0", "os": [ "darwin" ], diff --git a/ironfish-rust-nodejs/npm/darwin-x64/package.json b/ironfish-rust-nodejs/npm/darwin-x64/package.json index 7a628af100..fe1e1f31d4 100644 --- a/ironfish-rust-nodejs/npm/darwin-x64/package.json +++ b/ironfish-rust-nodejs/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-darwin-x64", - "version": "2.2.0", + "version": "2.3.0", "os": [ "darwin" ], diff --git a/ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json b/ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json index 4f20886d2b..c301658348 100644 --- a/ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json +++ b/ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-linux-arm64-gnu", - "version": "2.2.0", + "version": "2.3.0", "os": [ "linux" ], diff --git a/ironfish-rust-nodejs/npm/linux-arm64-musl/package.json b/ironfish-rust-nodejs/npm/linux-arm64-musl/package.json index 46c2e300d8..8402d2e60e 100644 --- a/ironfish-rust-nodejs/npm/linux-arm64-musl/package.json +++ b/ironfish-rust-nodejs/npm/linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-linux-arm64-musl", - "version": "2.2.0", + "version": "2.3.0", "os": [ "linux" ], diff --git a/ironfish-rust-nodejs/npm/linux-x64-gnu/package.json b/ironfish-rust-nodejs/npm/linux-x64-gnu/package.json index bd94b159c6..80644c94dc 100644 --- a/ironfish-rust-nodejs/npm/linux-x64-gnu/package.json +++ b/ironfish-rust-nodejs/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-linux-x64-gnu", - "version": "2.2.0", + "version": "2.3.0", "os": [ "linux" ], diff --git a/ironfish-rust-nodejs/npm/linux-x64-musl/package.json b/ironfish-rust-nodejs/npm/linux-x64-musl/package.json index a025b57df4..1156544516 100644 --- a/ironfish-rust-nodejs/npm/linux-x64-musl/package.json +++ b/ironfish-rust-nodejs/npm/linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-linux-x64-musl", - "version": "2.2.0", + "version": "2.3.0", "os": [ "linux" ], diff --git a/ironfish-rust-nodejs/npm/win32-x64-msvc/package.json b/ironfish-rust-nodejs/npm/win32-x64-msvc/package.json index 58a986901b..36ab12e616 100644 --- a/ironfish-rust-nodejs/npm/win32-x64-msvc/package.json +++ b/ironfish-rust-nodejs/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-win32-x64-msvc", - "version": "2.2.0", + "version": "2.3.0", "os": [ "win32" ], diff --git a/ironfish-rust-nodejs/package.json b/ironfish-rust-nodejs/package.json index 6fa3497271..7b19b42baf 100644 --- a/ironfish-rust-nodejs/package.json +++ b/ironfish-rust-nodejs/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs", - "version": "2.2.0", + "version": "2.3.0", "description": "Node.js bindings for Rust code required by the Iron Fish SDK", "main": "index.js", "types": "index.d.ts", diff --git a/ironfish/package.json b/ironfish/package.json index d078f320a0..aaf42966e7 100644 --- a/ironfish/package.json +++ b/ironfish/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/sdk", - "version": "2.2.0", + "version": "2.3.0", "description": "SDK for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js", @@ -22,7 +22,7 @@ "dependencies": { "@ethersproject/bignumber": "5.7.0", "@fast-csv/format": "4.3.5", - "@ironfish/rust-nodejs": "2.2.0", + "@ironfish/rust-nodejs": "2.3.0", "@napi-rs/blake-hash": "1.3.3", "axios": "0.21.4", "bech32": "2.0.0", @@ -100,4 +100,4 @@ "url": "https://github.com/iron-fish/ironfish/issues" }, "homepage": "https://ironfish.network" -} \ No newline at end of file +}