diff --git a/Cargo.lock b/Cargo.lock index 23367fe0b4..04f6df2125 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", @@ -510,7 +479,7 @@ checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "bitflags 1.3.2", "clap_lex", - "indexmap", + "indexmap 1.9.3", "textwrap", ] @@ -919,6 +888,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 +1198,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 +1208,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -1294,6 +1269,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" @@ -1330,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" @@ -1471,7 +1433,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]] @@ -1505,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)", @@ -1521,7 +1493,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#d2b082e3cd25d12073c1b113da941960c08fcb32" dependencies = [ "blake3", "chacha20 0.9.1", @@ -1534,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" @@ -1558,7 +1514,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", @@ -1566,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" @@ -1774,9 +1714,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 +1864,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 +1896,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", @@ -2009,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" @@ -2183,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" @@ -2252,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-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-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/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..139f97c4fd 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' @@ -148,7 +149,7 @@ export class Burn extends IronfishCommand { ) if (error) { - this.error(`${error.reason}`) + this.error(`${error.message}`) } amount = parsedAmount @@ -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..5ce07c67d4 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' @@ -212,7 +213,7 @@ export class Mint extends IronfishCommand { ) if (error) { - this.error(`${error.reason}`) + this.error(`${error.message}`) } amount = parsedAmount @@ -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/multisig/account/participants.ts b/ironfish-cli/src/commands/wallet/multisig/account/participants.ts new file mode 100644 index 0000000000..80e11ef213 --- /dev/null +++ b/ironfish-cli/src/commands/wallet/multisig/account/participants.ts @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { Flags } from '@oclif/core' +import { IronfishCommand } from '../../../../command' +import { RemoteFlags } from '../../../../flags' + +export class MultisigAccountParticipants extends IronfishCommand { + 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 group identities for', + }), + } + + async start(): Promise { + const { flags } = await this.parse(MultisigAccountParticipants) + + const client = await this.sdk.connectRpc() + + const response = await client.wallet.multisig.getAccountIdentities({ + account: flags.account, + }) + + for (const identity of response.content.identities) { + this.log(identity) + } + } +} diff --git a/ironfish-cli/src/commands/wallet/multisig/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..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, @@ -38,9 +37,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 b14a23a87b..5625b0ea21 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts @@ -5,21 +5,22 @@ 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' - static hidden = true 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({ @@ -31,18 +32,21 @@ export class DkgRound1Command extends IronfishCommand { async start(): Promise { const { flags } = await this.parse(DkgRound1Command) - let secretName = flags.secretName - if (!secretName) { - secretName = await CliUx.ux.prompt('Enter the name of the secret to use', { - required: true, - }) + const client = await this.sdk.connectRpc() + + let participantName = flags.participantName + if (!participantName) { + participantName = await selectSecret(client) } 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) { @@ -62,23 +66,21 @@ export class DkgRound1Command extends IronfishCommand { } } - const client = await this.sdk.connectRpc() - const response = await client.wallet.multisig.dkg.round1({ - secretName: secretName, + participantName, participants: identities.map((identity) => ({ identity })), 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 914fe74f2e..ac0fb45c2c 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts @@ -5,26 +5,26 @@ 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' - static hidden = true 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', - required: true, + aliases: ['name'], }), - 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, }), } @@ -32,57 +32,53 @@ export class DkgRound2Command extends IronfishCommand { async start(): Promise { const { flags } = await this.parse(DkgRound2Command) - let encryptedSecretPackage = flags.encryptedSecretPackage - if (!encryptedSecretPackage) { - encryptedSecretPackage = await CliUx.ux.prompt( - `Enter the encrypted secret package for secret ${flags.secretName}`, - { - required: true, - }, + const client = await this.sdk.connectRpc() + + 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 participant ${participantName}`, + { 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()) - - const client = await this.sdk.connectRpc() + round1PublicPackages = round1PublicPackages.map((i) => i.trim()) const response = await client.wallet.multisig.dkg.round2({ - secretName: flags.secretName, - encryptedSecretPackage, - publicPackages, + participantName, + 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 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('\nRound 2 Public Package:\n') + this.log(response.content.round2PublicPackage) + 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 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 new file mode 100644 index 0000000000..f65ff4ed20 --- /dev/null +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -0,0 +1,118 @@ +/* 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' +import { selectSecret } from '../../../../utils/multisig' + +export class DkgRound3Command extends IronfishCommand { + static description = 'Perform round3 of the DKG protocol for multisig account creation' + + static flags = { + ...RemoteFlags, + participantName: Flags.string({ + char: 'n', + description: 'The name of the secret to use for decryption during DKG', + aliases: ['name'], + }), + accountName: Flags.string({ + char: 'a', + description: 'The name to set for the imported account', + }), + round2SecretPackage: Flags.string({ + char: 'e', + 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 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 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, + }), + } + + async start(): Promise { + const { flags } = await this.parse(DkgRound3Command) + + const client = await this.sdk.connectRpc() + + let participantName = flags.participantName + if (!participantName) { + participantName = await selectSecret(client) + } + + let round2SecretPackage = flags.round2SecretPackage + if (!round2SecretPackage) { + round2SecretPackage = await CliUx.ux.prompt( + `Enter the encrypted secret package for participant ${participantName}`, + { + required: true, + }, + ) + } + + 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', + { + required: true, + }, + ) + round1PublicPackages = input.split(',') + + if (round1PublicPackages.length < 2) { + this.error( + 'Must include a round 1 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 round 2 public packages, separated by commas, one for each participant', + { + required: true, + }, + ) + round2PublicPackages = input.split(',') + + // 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 should be 1 less than the number of round 1 public packages', + ) + } + } + round2PublicPackages = round2PublicPackages.map((i) => i.trim()) + + const response = await client.wallet.multisig.dkg.round3({ + participantName, + accountName: flags.accountName, + round2SecretPackage, + round1PublicPackages, + round2PublicPackages, + }) + + this.log() + this.log( + `Account ${response.content.name} imported with public address: ${response.content.publicAddress}`, + ) + } +} 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.') } } diff --git a/ironfish-cli/src/commands/wallet/multisig/participants/index.ts b/ironfish-cli/src/commands/wallet/multisig/participants/index.ts index 7f6af9cfb9..95df0936ef 100644 --- a/ironfish-cli/src/commands/wallet/multisig/participants/index.ts +++ b/ironfish-cli/src/commands/wallet/multisig/participants/index.ts @@ -1,32 +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 { Flags } from '@oclif/core' +import { CliUx } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' -export class MultisigAccountParticipants extends IronfishCommand { - static description = `List the participant identities for a multisig account` +export class MultisigParticipants extends IronfishCommand { + static description = 'List out all the participant names and identities' static flags = { ...RemoteFlags, - account: Flags.string({ - char: 'f', - description: 'The account to list identities for', - }), } async start(): Promise { - const { flags } = await this.parse(MultisigAccountParticipants) - const client = await this.sdk.connectRpc() + const response = await client.wallet.multisig.getIdentities() - const response = await client.wallet.multisig.getAccountIdentities({ - account: flags.account, - }) - - for (const identity of response.content.identities) { - this.log(identity) + 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, + }, + ) } } 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(',') 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/post.ts b/ironfish-cli/src/commands/wallet/post.ts index 69b862c4b1..4eb93b4413 100644 --- a/ironfish-cli/src/commands/wallet/post.ts +++ b/ironfish-cli/src/commands/wallet/post.ts @@ -1,16 +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' @@ -26,6 +22,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 +39,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) @@ -111,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/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-cli/src/commands/wallet/send.ts b/ironfish-cli/src/commands/wallet/send.ts index b7cee6dd41..0e18f6a431 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' @@ -162,7 +163,7 @@ export class Send extends IronfishCommand { ) if (error) { - this.error(`${error.reason}`) + this.error(`${error.message}`) } amount = parsedAmount @@ -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/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 + } + } +} 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) } } 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/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/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/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-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' 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 +} 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-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-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/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/index.d.ts b/ironfish-rust-nodejs/index.d.ts index 35812ac3d0..8fda5d4e5b 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 @@ -252,17 +250,23 @@ export namespace multisig { } export function dkgRound1(selfIdentity: string, minSigners: number, participantIdentities: Array): DkgRound1Packages export interface DkgRound1Packages { - encryptedSecretPackage: string - publicPackage: string - } - export function dkgRound2(secret: string, encryptedSecretPackage: string, publicPackages: Array): DkgRound2Packages - export interface DkgRound2PublicPackage { - recipientIdentity: string - publicPackage: string + round1SecretPackage: string + round1PublicPackage: string } + export function dkgRound2(secret: string, round1SecretPackage: string, round1PublicPackages: Array): DkgRound2Packages export interface DkgRound2Packages { - encryptedSecretPackage: string - publicPackages: Array + round2SecretPackage: string + round2PublicPackage: string + } + 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 { 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/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-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, - }) -} diff --git a/ironfish-rust-nodejs/src/multisig.rs b/ironfish-rust-nodejs/src/multisig.rs index e3acf0d6e1..957676b23a 100644 --- a/ironfish-rust-nodejs/src/multisig.rs +++ b/ironfish-rust-nodejs/src/multisig.rs @@ -5,22 +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::{import_secret_package, 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")] @@ -29,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")] @@ -386,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 (round1_secret_package, round1_public_package) = dkg::round1::round1( &self_identity, min_signers, &participant_identities, @@ -395,62 +386,92 @@ 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(public_packages)?; + let round1_public_packages = try_deserialize(round1_public_packages, |bytes| { + dkg::round1::PublicPackage::deserialize_from(bytes) + })?; + let round1_secret_package = hex_to_vec_bytes(&round1_secret_package).map_err(to_napi_err)?; - let secret_package = import_secret_package( - &hex_to_vec_bytes(&encrypted_secret_package).map_err(to_napi_err)?, + let (round2_secret_package, round2_public_package) = dkg::round2::round2( &secret, - ) - .map_err(to_napi_err)?; - - let (encrypted_secret_package, public_packages) = ironfish_frost::dkg::round2::round2( - &secret.to_identity(), - &secret_package, - &public_packages, + &round1_secret_package, + &round1_public_packages, thread_rng(), ) .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, + 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 DkgRound2PublicPackage { - pub recipient_identity: String, - pub public_package: String, +pub struct DkgRound2Packages { + pub round2_secret_package: String, + pub round2_public_package: String, } #[napi(object, namespace = "multisig")] -pub struct DkgRound2Packages { - pub encrypted_secret_package: String, - pub public_packages: Vec, +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::CombinedPublicPackage::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/package.json b/ironfish/package.json index 739c20178b..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", @@ -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", 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/importAccount.test.ts b/ironfish/src/rpc/routes/wallet/importAccount.test.ts index cc956f7869..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 @@ -390,13 +418,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..da4eff4d3e 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 }, ) } @@ -65,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) 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 { 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({ - 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 - const secretValue = await routeTest.node.wallet.walletDb.getMultisigSecretByName(secretName) + 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.encryptedSecretPackage, 'hex')) + 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,28 +70,29 @@ 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) expect(response.content).toMatchObject({ - encryptedSecretPackage: expect.any(String), - publicPackage: expect.any(String), + round1SecretPackage: expect.any(String), + round1PublicPackage: expect.any(String), }) }) 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 66502eebea..5d599e6e4f 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.ts @@ -9,19 +9,19 @@ import { routes } from '../../../router' import { AssertHasRpcContext } from '../../../rpcContext' export type DkgRound1Request = { - secretName: string + participantName: string minSigners: number participants: Array<{ identity: string }> } export type DkgRound1Response = { - encryptedSecretPackage: string - publicPackage: string + round1SecretPackage: string + round1PublicPackage: string } export const DkgRound1RequestSchema: yup.ObjectSchema = yup .object({ - secretName: yup.string().defined(), + participantName: yup.string().defined(), minSigners: yup.number().defined(), participants: yup .array() @@ -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() @@ -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 05cf72cd63..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,46 +7,46 @@ 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, - encryptedSecretPackage: round1Response1.content.encryptedSecretPackage, - publicPackages: [ - round1Response1.content.publicPackage, - round1Response2.content.publicPackage, + participantName: participantName1, + 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'], + participantName: 'fakeName', + 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 2b5aff4f1b..3d87ae5b8b 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.ts @@ -9,37 +9,28 @@ import { routes } from '../../../router' import { AssertHasRpcContext } from '../../../rpcContext' export type DkgRound2Request = { - secretName: string - encryptedSecretPackage: string - publicPackages: Array + participantName: string + round1SecretPackage: string + round1PublicPackages: Array } export type DkgRound2Response = { - encryptedSecretPackage: string - publicPackages: Array<{ recipientIdentity: string; publicPackage: 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(), + participantName: yup.string().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 - .array( - yup - .object({ - recipientIdentity: yup.string().defined(), - publicPackage: yup.string().defined(), - }) - .defined(), - ) - .defined(), + round2SecretPackage: yup.string().defined(), + round2PublicPackage: yup.string().defined(), }) .defined() @@ -49,12 +40,12 @@ routes.register( async (request, node): Promise => { AssertHasRpcContext(request, node, 'wallet') - const { secretName, encryptedSecretPackage, publicPackages } = 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, ) @@ -62,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 new file mode 100644 index 0000000000..a3e9ea1f25 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts @@ -0,0 +1,254 @@ +/* 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' + +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 participantNames = ['secret-0', 'secret-1', 'secret-2'] + const accountNames = ['account-0', 'account-1', 'account-2'] + + // Create participants and retrieve their identities + await Promise.all( + participantNames.map((name) => + routeTest.client.wallet.multisig.createParticipant({ name }), + ), + ) + const participants = await Promise.all( + participantNames.map( + async (name) => (await routeTest.client.wallet.multisig.getIdentity({ name })).content, + ), + ) + + // Perform DKG round 1 + const round1Packages = await Promise.all( + participantNames.map((participantName) => + routeTest.client.wallet.multisig.dkg.round1({ + participantName, + minSigners: 2, + participants, + }), + ), + ) + + // Perform DKG round 2 + const round2Packages = await Promise.all( + participantNames.map((participantName, index) => + routeTest.client.wallet.multisig.dkg.round2({ + participantName, + round1SecretPackage: round1Packages[index].content.round1SecretPackage, + round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), + }), + ), + ) + + // Perform DKG round 3 + const round3Responses = await Promise.all( + participantNames.map((participantName, index) => + routeTest.client.wallet.multisig.dkg.round3({ + participantName, + accountName: accountNames[index], + round2SecretPackage: round2Packages[index].content.round2SecretPackage, + round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), + round2PublicPackages: round2Packages.map((pkg) => pkg.content.round2PublicPackage), + }), + ), + ) + + // Check that all accounts that got imported after round 3 have the same public address + const publicKeys = await Promise.all( + accountNames.map( + async (account) => + ( + await routeTest.client.wallet.getAccountPublicKey({ account }) + ).content.publicKey, + ), + ) + const expectedPublicKey = publicKeys[0] + 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.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).sort() + for (const accountName of accountNames) { + const account = routeTest.wallet.getAccountByName(accountName) + Assert.isNotNull(account) + const knownIdentities = account + .getMultisigParticipantIdentities() + .map((identity) => identity.toString('hex')) + .sort() + expect(knownIdentities).toStrictEqual(expectedIdentities) + } + }) + + it('should fail if not all round 1 packages are passed as an input', async () => { + const participantNames = ['secret-0', 'secret-1', 'secret-2'] + + // Create participants and retrieve their identities + await Promise.all( + participantNames.map((name) => + routeTest.client.wallet.multisig.createParticipant({ name }), + ), + ) + const participants = await Promise.all( + participantNames.map( + async (name) => (await routeTest.client.wallet.multisig.getIdentity({ name })).content, + ), + ) + + // Perform DKG round 1 + const round1Packages = await Promise.all( + participantNames.map((participantName) => + routeTest.client.wallet.multisig.dkg.round1({ + participantName, + minSigners: 2, + participants, + }), + ), + ) + + // Perform DKG round 2 + const round2Packages = await Promise.all( + participantNames.map((participantName, index) => + routeTest.client.wallet.multisig.dkg.round2({ + participantName, + round1SecretPackage: round1Packages[index].content.round1SecretPackage, + round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), + }), + ), + ) + + // Perform DKG round 3 + await expect( + routeTest.client.wallet.multisig.dkg.round3({ + participantName: participantNames[0], + round2SecretPackage: round2Packages[0].content.round2SecretPackage, + round1PublicPackages: removeOneElement( + round1Packages.map((pkg) => pkg.content.round1PublicPackage), + ), + round2PublicPackages: round2Packages.map((pkg) => pkg.content.round2PublicPackage), + }), + ).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 participantNames = ['secret-0', 'secret-1', 'secret-2'] + + // Create participants and retrieve their identities + await Promise.all( + participantNames.map((name) => + routeTest.client.wallet.multisig.createParticipant({ name }), + ), + ) + const participants = await Promise.all( + participantNames.map( + async (name) => (await routeTest.client.wallet.multisig.getIdentity({ name })).content, + ), + ) + + // Perform DKG round 1 + const round1Packages = await Promise.all( + participantNames.map((participantName) => + routeTest.client.wallet.multisig.dkg.round1({ + participantName, + minSigners: 2, + participants, + }), + ), + ) + + // Perform DKG round 2 + const round2Packages = await Promise.all( + participantNames.map((participantName, index) => + routeTest.client.wallet.multisig.dkg.round2({ + participantName, + round1SecretPackage: round1Packages[index].content.round1SecretPackage, + round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), + }), + ), + ) + + // Perform DKG round 3 + await expect( + routeTest.client.wallet.multisig.dkg.round3({ + 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 `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), + ), + }), + ).rejects.toThrow('invalid input: expected 2 round 2 public packages, got 1') + }) + + it('should fail passing the wrong round 2 secret package', async () => { + const participantNames = ['secret-0', 'secret-1', 'secret-2'] + + // Create participants and retrieve their identities + await Promise.all( + participantNames.map((name) => + routeTest.client.wallet.multisig.createParticipant({ name }), + ), + ) + const participants = await Promise.all( + participantNames.map( + async (name) => (await routeTest.client.wallet.multisig.getIdentity({ name })).content, + ), + ) + + // Perform DKG round 1 + const round1Packages = await Promise.all( + participantNames.map((participantName) => + routeTest.client.wallet.multisig.dkg.round1({ + participantName, + minSigners: 2, + participants, + }), + ), + ) + + // Perform DKG round 2 + const round2Packages = await Promise.all( + participantNames.map((participantName, index) => + routeTest.client.wallet.multisig.dkg.round2({ + participantName, + round1SecretPackage: round1Packages[index].content.round1SecretPackage, + round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), + }), + ), + ) + + // Perform DKG round 3 + await expect( + routeTest.client.wallet.multisig.dkg.round3({ + participantName: participantNames[0], + 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/dkg/round3.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts new file mode 100644 index 0000000000..92bdf536ae --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts @@ -0,0 +1,112 @@ +/* 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 = { + participantName: string + round2SecretPackage: string + round1PublicPackages: Array + round2PublicPackages: Array + accountName?: string + rescan?: boolean +} + +export type DkgRound3Response = { + name: string + publicAddress: string +} + +export const DkgRound3RequestSchema: yup.ObjectSchema = yup + .object({ + participantName: yup.string().defined(), + round2SecretPackage: yup.string().defined(), + 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() + +export const DkgRound3ResponseSchema: yup.ObjectSchema = yup + .object({ + name: yup.string().defined(), + publicAddress: yup.string().defined(), + }) + .defined() + +routes.register( + `${ApiNamespace.wallet}/multisig/dkg/round3`, + DkgRound3RequestSchema, + async (request, node): Promise => { + Assert.isInstanceOf(node, FullNode) + + const { participantName } = request.data + const multisigSecret = await node.wallet.walletDb.getMultisigSecretByName(participantName) + + if (!multisigSecret) { + throw new RpcValidationError( + `Multisig secret with name '${participantName}' 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: request.data.accountName ?? participantName, + version: ACCOUNT_SCHEMA_VERSION, + createdAt: null, + spendingKey: null, + viewKey, + incomingViewKey, + outgoingViewKey, + publicAddress, + proofAuthorizingKey, + multisigKeys: { + identity, + keyPackage, + publicKeyPackage, + }, + } + + const account = await node.wallet.importAccount(accountImport) + + 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, + publicAddress: account.publicAddress, + }) + }, +) diff --git a/ironfish/src/rpc/routes/wallet/multisig/getAccountIdentities.ts b/ironfish/src/rpc/routes/wallet/multisig/getAccountIdentities.ts index 2859795557..e641373dba 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/getAccountIdentities.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/getAccountIdentities.ts @@ -1,7 +1,6 @@ /* 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 { AssertMultisig } from '../../../../wallet' import { ApiNamespace } from '../../namespaces' @@ -39,10 +38,9 @@ routes.register identity.toString('hex')) + const identities = account + .getMultisigParticipantIdentities() + .map((identity) => identity.toString('hex')) request.end({ identities }) }, 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..f6a999b32c 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( + participantNames: Array, + ): Promise> { + return Promise.all( + participantNames.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,93 @@ 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({ + participantName: name, + minSigners, + participants, + }), + ), + ) + + // perform dkg round 2 + const round2Packages = await Promise.all( + participants.map(({ name }, index) => + routeTest.client.wallet.multisig.dkg.round2({ + participantName: name, + round1SecretPackage: round1Packages[index].content.round1SecretPackage, + round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), + }), + ), + ) + + // perform dkg round 3 + const participantAccounts = await Promise.all( + participants.map(async ({ name }, index) => { + await routeTest.client.wallet.multisig.dkg.round3({ + participantName: name, + 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) + 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 +279,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 +302,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 +326,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) 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) { 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/utils/currency.test.ts b/ironfish/src/utils/currency.test.ts index e4b8772046..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') }) }) @@ -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 2644cb9661..2224042a6b 100644 --- a/ironfish/src/utils/currency.ts +++ b/ironfish/src/utils/currency.ts @@ -5,12 +5,9 @@ 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 { - static locale?: string - /** * Serializes ore as iron with up to 8 decimal places */ @@ -62,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 @@ -106,27 +111,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 */ @@ -149,29 +133,13 @@ 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 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) -function assetMetadataWithDefaults( +export function assetMetadataWithDefaults( assetId?: string, verifiedAssetMetadata?: { decimals?: number 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 { 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..8c6bd223dd 100644 --- a/ironfish/src/wallet/wallet.test.slow.ts +++ b/ironfish/src/wallet/wallet.test.slow.ts @@ -1362,11 +1362,9 @@ 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() + .map((identity) => identity.toString('hex')) expect(identities.sort()).toEqual(storedIdentities.sort()) }) }) diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index 9fb9a1e2cb..161d88d2f3 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, @@ -1526,7 +1560,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 +1573,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, @@ -1607,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 - } - } } 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/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 f587186fd7..13502041eb 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" @@ -1080,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" 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==