From ce4b7e7880f2382b9eb1f352365de5cb1343e6a9 Mon Sep 17 00:00:00 2001 From: jowparks Date: Tue, 16 Jan 2024 14:12:18 -0800 Subject: [PATCH 01/38] refactor posting of proposed/unsigned transaction (#4539) --- ironfish-rust/src/transaction/mod.rs | 178 +++++----------------- ironfish-rust/src/transaction/tests.rs | 9 +- ironfish-rust/src/transaction/unsigned.rs | 31 ++++ 3 files changed, 79 insertions(+), 139 deletions(-) diff --git a/ironfish-rust/src/transaction/mod.rs b/ironfish-rust/src/transaction/mod.rs index bf6efd09b5..eef00bbff3 100644 --- a/ironfish-rust/src/transaction/mod.rs +++ b/ironfish-rust/src/transaction/mod.rs @@ -190,28 +190,19 @@ impl ProposedTransaction { Ok(()) } - pub fn build( + pub fn add_change_notes( &mut self, - proof_generation_key: ProofGenerationKey, - view_key: ViewKey, - outgoing_view_key: OutgoingViewKey, - public_address: PublicAddress, change_goes_to: Option, - intended_transaction_fee: u64, - ) -> Result { - // The public key after randomization has been applied. This is used - // during signature verification. Referred to as `rk` in the literature - // Calculated from the authorizing key and the public_key_randomness. - let randomized_public_key = redjubjub::PublicKey(view_key.authorizing_key.into()) - .randomize(self.public_key_randomness, *SPENDING_KEY_GENERATOR); - + public_address: PublicAddress, + intended_transaction_fee: i64, + ) -> Result<(), IronfishError> { let mut change_notes = vec![]; for (asset_id, value) in self.value_balances.iter() { let is_native_asset = asset_id == &NATIVE_ASSET; let change_amount = match is_native_asset { - true => *value - i64::try_from(intended_transaction_fee)?, + true => *value - intended_transaction_fee, false => *value, }; @@ -235,6 +226,22 @@ impl ProposedTransaction { for change_note in change_notes { self.add_output(change_note)?; } + Ok(()) + } + + pub fn build( + &mut self, + proof_generation_key: ProofGenerationKey, + view_key: ViewKey, + outgoing_view_key: OutgoingViewKey, + public_address: PublicAddress, + intended_transaction_fee: i64, + ) -> Result { + // The public key after randomization has been applied. This is used + // during signature verification. Referred to as `rk` in the literature + // Calculated from the authorizing key and the public_key_randomness. + let randomized_public_key = redjubjub::PublicKey(view_key.authorizing_key.into()) + .randomize(self.public_key_randomness, *SPENDING_KEY_GENERATOR); let mut unsigned_spends = Vec::with_capacity(self.spends.len()); for spend in &self.spends { @@ -295,7 +302,7 @@ impl ProposedTransaction { outputs: output_descriptions, spends: unsigned_spends, version: self.version, - fee: i64::try_from(intended_transaction_fee)?, + fee: intended_transaction_fee, binding_signature, randomized_public_key, public_key_randomness: self.public_key_randomness, @@ -319,38 +326,19 @@ impl ProposedTransaction { change_goes_to: Option, intended_transaction_fee: u64, ) -> Result { - let mut change_notes = vec![]; + let public_address = spender_key.public_address(); - for (asset_id, value) in self.value_balances.iter() { - let is_native_asset = asset_id == &NATIVE_ASSET; - - let change_amount = match is_native_asset { - true => *value - i64::try_from(intended_transaction_fee)?, - false => *value, - }; - - if change_amount < 0 { - return Err(IronfishError::new(IronfishErrorKind::InvalidBalance)); - } - if change_amount > 0 { - let change_address = change_goes_to.unwrap_or_else(|| spender_key.public_address()); - let change_note = Note::new( - change_address, - change_amount as u64, // we checked it was positive - "", - *asset_id, - spender_key.public_address(), - ); + let i64_fee = i64::try_from(intended_transaction_fee)?; + self.add_change_notes(change_goes_to, public_address, i64_fee)?; - change_notes.push(change_note); - } - } - - for change_note in change_notes { - self.add_output(change_note)?; - } - - self._partial_post(spender_key) + let unsigned = self.build( + spender_key.sapling_proof_generation_key(), + spender_key.view_key().clone(), + spender_key.outgoing_view_key().clone(), + public_address, + i64_fee, + )?; + unsigned.sign(spender_key) } /// Special case for posting a miners fee transaction. Miner fee transactions @@ -383,7 +371,14 @@ impl ProposedTransaction { for output in &mut self.outputs { output.set_is_miners_fee(); } - self._partial_post(spender_key) + let unsigned = self.build( + spender_key.sapling_proof_generation_key(), + spender_key.view_key().clone(), + spender_key.outgoing_view_key().clone(), + spender_key.public_address(), + *self.value_balances.fee(), + )?; + unsigned.sign(spender_key) } /// Get the expiration sequence for this transaction @@ -396,97 +391,6 @@ impl ProposedTransaction { self.expiration = sequence; } - // Post transaction without much validation. - fn _partial_post(&self, spender_key: &SaplingKey) -> Result { - // Generate randomized public key - - // The public key after randomization has been applied. This is used - // during signature verification. Referred to as `rk` in the literature - // Calculated from the authorizing key and the public_key_randomness. - let randomized_public_key = - redjubjub::PublicKey(spender_key.view_key.authorizing_key.into()) - .randomize(self.public_key_randomness, *SPENDING_KEY_GENERATOR); - - // Build descriptions - let mut unsigned_spends = Vec::with_capacity(self.spends.len()); - for spend in &self.spends { - unsigned_spends.push(spend.build( - &spender_key.sapling_proof_generation_key(), - spender_key.view_key(), - &self.public_key_randomness, - &randomized_public_key, - )?); - } - - let mut output_descriptions = Vec::with_capacity(self.outputs.len()); - for output in &self.outputs { - output_descriptions.push(output.build( - &spender_key.sapling_proof_generation_key(), - spender_key.outgoing_view_key(), - &self.public_key_randomness, - &randomized_public_key, - )?); - } - - let mut unsigned_mints = Vec::with_capacity(self.mints.len()); - for mint in &self.mints { - unsigned_mints.push(mint.build( - &spender_key.sapling_proof_generation_key(), - &spender_key.public_address(), - &self.public_key_randomness, - &randomized_public_key, - )?); - } - - let mut burn_descriptions = Vec::with_capacity(self.burns.len()); - for burn in &self.burns { - burn_descriptions.push(burn.build()); - } - - // Create the transaction signature hash - let data_to_sign = self.transaction_signature_hash( - &unsigned_spends, - &output_descriptions, - &unsigned_mints, - &burn_descriptions, - &randomized_public_key, - )?; - - // Create and verify binding signature keys - let (binding_signature_private_key, binding_signature_public_key) = - self.binding_signature_keys(&unsigned_mints, &burn_descriptions)?; - - let binding_signature = self.binding_signature( - &binding_signature_private_key, - &binding_signature_public_key, - &data_to_sign, - )?; - - // Sign spends now that we have the data needed to be signed - let mut spend_descriptions = Vec::with_capacity(unsigned_spends.len()); - for spend in unsigned_spends.drain(0..) { - spend_descriptions.push(spend.sign(spender_key, &data_to_sign)?); - } - - // Sign mints now that we have the data needed to be signed - let mut mint_descriptions = Vec::with_capacity(unsigned_mints.len()); - for mint in unsigned_mints.drain(0..) { - mint_descriptions.push(mint.sign(spender_key, &data_to_sign)?); - } - - Ok(Transaction { - version: self.version, - expiration: self.expiration, - fee: *self.value_balances.fee(), - spends: spend_descriptions, - outputs: output_descriptions, - mints: mint_descriptions, - burns: burn_descriptions, - binding_signature, - randomized_public_key, - }) - } - /// Calculate a hash of the transaction data. This hash is what gets signed /// by the private keys to verify that the transaction actually happened. /// diff --git a/ironfish-rust/src/transaction/tests.rs b/ironfish-rust/src/transaction/tests.rs index 355254ae14..a8f79fa9da 100644 --- a/ironfish-rust/src/transaction/tests.rs +++ b/ironfish-rust/src/transaction/tests.rs @@ -227,14 +227,19 @@ fn test_proposed_transaction_build() { transaction.add_output(out_note).unwrap(); assert_eq!(transaction.outputs.len(), 1); + let public_address = spender_key.public_address(); + let intended_fee = 1; + transaction + .add_change_notes(Some(public_address), public_address, intended_fee) + .expect("should be able to add change notes"); + let unsigned_transaction = transaction .build( spender_key.sapling_proof_generation_key(), spender_key.view_key().clone(), spender_key.outgoing_view_key().clone(), spender_key.public_address(), - Some(spender_key.public_address()), - 1, + intended_fee, ) .expect("should be able to build unsigned transaction"); diff --git a/ironfish-rust/src/transaction/unsigned.rs b/ironfish-rust/src/transaction/unsigned.rs index cf309bd696..fc2a74005d 100644 --- a/ironfish-rust/src/transaction/unsigned.rs +++ b/ironfish-rust/src/transaction/unsigned.rs @@ -9,6 +9,7 @@ use std::io::{self, Write}; use crate::{ errors::IronfishError, serializing::read_scalar, transaction::Blake2b, OutputDescription, + SaplingKey, Transaction, }; use super::{ @@ -178,4 +179,34 @@ impl UnsignedTransaction { hash_result[..].clone_from_slice(hasher.finalize().as_ref()); Ok(hash_result) } + + // Post transaction without much validation. + pub fn sign(&self, spender_key: &SaplingKey) -> Result { + // Create the transaction signature hash + let data_to_sign = self.transaction_signature_hash()?; + + // Sign spends now that we have the data needed to be signed + let mut spend_descriptions = Vec::with_capacity(self.spends.len()); + for spend in self.spends.clone() { + spend_descriptions.push(spend.sign(spender_key, &data_to_sign)?); + } + + // Sign mints now that we have the data needed to be signed + let mut mint_descriptions = Vec::with_capacity(self.mints.len()); + for mint in self.mints.clone() { + mint_descriptions.push(mint.sign(spender_key, &data_to_sign)?); + } + + Ok(Transaction { + version: self.version, + expiration: self.expiration, + fee: self.fee, + spends: spend_descriptions, + outputs: self.outputs.clone(), + mints: mint_descriptions, + burns: self.burns.clone(), + binding_signature: self.binding_signature, + randomized_public_key: self.randomized_public_key.clone(), + }) + } } From 7ec915b9b1fc089148ec82488e3555ba4f8bd5eb Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Tue, 16 Jan 2024 15:10:06 -0800 Subject: [PATCH 02/38] adds optional multiSigKeys to AccountValue (#4538) * adds optional multiSigKeys to AccountValue multiSigKeys includes three fields needed for an account to participate in a FROST signing group: identifier, keyPackage, and proofGenerationKey the identifier field is still to be determined as either a public key identifier for the participant, or a private key from which we can derive the public key identifier the keyPackage is the key package generated for the participant during key generation and contains the participant's authorizing key shard the proofGenerationKey is a key shared by the signing group that is needed for any participant to generate spend or mint proofs * updates test so that spendingKey is null when multiSigKeys is defined --- .../src/wallet/walletdb/accountValue.test.ts | 27 ++++++++++++++++++ ironfish/src/wallet/walletdb/accountValue.ts | 28 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/ironfish/src/wallet/walletdb/accountValue.test.ts b/ironfish/src/wallet/walletdb/accountValue.test.ts index 51deb19d4b..fda4eab513 100644 --- a/ironfish/src/wallet/walletdb/accountValue.test.ts +++ b/ironfish/src/wallet/walletdb/accountValue.test.ts @@ -22,6 +22,33 @@ describe('AccountValueEncoding', () => { hash: Buffer.alloc(32, 0), sequence: 1, }, + multiSigKeys: undefined, + } + const buffer = encoder.serialize(value) + const deserializedValue = encoder.deserialize(buffer) + expect(deserializedValue).toEqual(value) + }) + + it('serializes an object with multiSigKeys into a buffer and deserializes to the original object', () => { + const encoder = new AccountValueEncoding() + + const key = generateKey() + const value: AccountValue = { + id: 'id', + name: 'foobar๐Ÿ‘๏ธ๐Ÿƒ๐ŸŸ', + incomingViewKey: key.incomingViewKey, + outgoingViewKey: key.outgoingViewKey, + publicAddress: key.publicAddress, + // NOTE: accounts with multiSigKeys should not have spendingKey + spendingKey: null, + viewKey: key.viewKey, + version: 1, + createdAt: null, + multiSigKeys: { + identifier: 'a', + keyPackage: 'b', + proofGenerationKey: 'c', + }, } const buffer = encoder.serialize(value) const deserializedValue = encoder.deserialize(buffer) diff --git a/ironfish/src/wallet/walletdb/accountValue.ts b/ironfish/src/wallet/walletdb/accountValue.ts index 657053c875..fbafdc32fb 100644 --- a/ironfish/src/wallet/walletdb/accountValue.ts +++ b/ironfish/src/wallet/walletdb/accountValue.ts @@ -21,6 +21,11 @@ export interface AccountValue { outgoingViewKey: string publicAddress: string createdAt: HeadValue | null + multiSigKeys?: { + identifier: string + keyPackage: string + proofGenerationKey: string + } } export type AccountImport = Omit @@ -31,6 +36,7 @@ export class AccountValueEncoding implements IDatabaseEncoding { let flags = 0 flags |= Number(!!value.spendingKey) << 0 flags |= Number(!!value.createdAt) << 1 + flags |= Number(!!value.multiSigKeys) << 2 bw.writeU8(flags) bw.writeU16(value.version) bw.writeVarString(value.id, 'utf8') @@ -48,6 +54,12 @@ export class AccountValueEncoding implements IDatabaseEncoding { bw.writeBytes(encoding.serialize(value.createdAt)) } + if (value.multiSigKeys) { + bw.writeVarString(value.multiSigKeys.identifier, 'utf8') + bw.writeVarString(value.multiSigKeys.keyPackage, 'utf8') + bw.writeVarString(value.multiSigKeys.proofGenerationKey, 'utf8') + } + return bw.render() } @@ -57,6 +69,7 @@ export class AccountValueEncoding implements IDatabaseEncoding { const version = reader.readU16() const hasSpendingKey = flags & (1 << 0) const hasCreatedAt = flags & (1 << 1) + const hasMultiSigKeys = flags & (1 << 2) const id = reader.readVarString('utf8') const name = reader.readVarString('utf8') const spendingKey = hasSpendingKey ? reader.readBytes(KEY_LENGTH).toString('hex') : null @@ -71,6 +84,15 @@ export class AccountValueEncoding implements IDatabaseEncoding { createdAt = encoding.deserialize(reader.readBytes(encoding.nonNullSize)) } + let multiSigKeys = undefined + if (hasMultiSigKeys) { + multiSigKeys = { + identifier: reader.readVarString('utf8'), + keyPackage: reader.readVarString('utf8'), + proofGenerationKey: reader.readVarString('utf8'), + } + } + return { version, id, @@ -81,6 +103,7 @@ export class AccountValueEncoding implements IDatabaseEncoding { spendingKey, publicAddress, createdAt, + multiSigKeys, } } @@ -101,6 +124,11 @@ export class AccountValueEncoding implements IDatabaseEncoding { const encoding = new NullableHeadValueEncoding() size += encoding.nonNullSize } + if (value.multiSigKeys) { + size += bufio.sizeVarString(value.multiSigKeys.identifier, 'utf8') + size += bufio.sizeVarString(value.multiSigKeys.keyPackage, 'utf8') + size += bufio.sizeVarString(value.multiSigKeys.proofGenerationKey, 'utf8') + } return size } From b4efbc03d084e7840f8175e114636848777269d2 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Tue, 16 Jan 2024 21:02:35 -0800 Subject: [PATCH 03/38] Rahul/ifl 2105 add note selection to walletsend (#4545) * accepts notes as input * send transaction accepts notes as input * adding test for send transaction with notes * adding test for sending transaction with note * moving to a slow test --- .../sendTransaction.test.slow.ts.fixture | 47 +++++++++++++ .../wallet/sendTransaction.test.slow.ts | 69 +++++++++++++++++++ .../src/rpc/routes/wallet/sendTransaction.ts | 6 ++ ironfish/src/wallet/wallet.ts | 2 + 4 files changed, 124 insertions(+) create mode 100644 ironfish/src/rpc/routes/wallet/__fixtures__/sendTransaction.test.slow.ts.fixture create mode 100644 ironfish/src/rpc/routes/wallet/sendTransaction.test.slow.ts diff --git a/ironfish/src/rpc/routes/wallet/__fixtures__/sendTransaction.test.slow.ts.fixture b/ironfish/src/rpc/routes/wallet/__fixtures__/sendTransaction.test.slow.ts.fixture new file mode 100644 index 0000000000..dd47f32098 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/__fixtures__/sendTransaction.test.slow.ts.fixture @@ -0,0 +1,47 @@ +{ + "Route wallet/sendTransaction (with note selection) spends the specified notes": [ + { + "version": 2, + "id": "26cc1404-2698-48fe-8349-b65a9b89ca8e", + "name": "accountA", + "spendingKey": "55ad30a74152cad39b303f0259c7e864f04f02d6c5d1a32b5ff4557c212e617f", + "viewKey": "5caf4408f59c80f92433f4a07eb64ca5398893a3858bd0c29b86a0a14ba8fdaa91a06922c5c8ad29ce69ccbc5ab16b1c9415bc3ae7d8e7f640b963b02c1812e2", + "incomingViewKey": "2450d4985be3792f8beba1c4eb4f0f9b5cbd715c17adb70c38d1728399c00202", + "outgoingViewKey": "a83d870c4dd8f74477eabb785e9296ac35730d3d91a89ca2274523fabbd7232c", + "publicAddress": "d5cafca4182d5aa93177f5c123755657249409e78b278f8d6695f3396a79f36e", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86", + "noteCommitment": { + "type": "Buffer", + "data": "base64:SQsBHFSHKtrw79S/QFeog+P67j4T4uznBUlUeSfVN2I=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:u6b3RcbXxAlnEN/1dZ/UFL4RxbwQE5QGyosANE+3RKs=" + }, + "target": "9282972777491357380673661573939192202192629606981189395159182914949423", + "randomness": "0", + "timestamp": 1705452824018, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AgAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAPh72zI708bh0USjaFbYzl5iVKdOzSoQJJHA63+9qmNmt21B2guUWQUo4+xjElOwXbRBJGds5vtUE+qaAU2jMXbZyl1i5n1X+E8risBkqBcqI+52CpcKPzuf6/TZjzEdfVQfDgMEpVeOuKr6fQm8GyW1HdRA5QgaCmWPTyvunM68HqTOeATARzWQKL2QTb9/kd6RezV4raOuSNSn5se/cjy5JLvRFuw3syy1n9p7gppmELAAK/ZFNMfpth/w0P4suhwulyZgCQagb3GOE+Wvji1WhqnmNPhWUCB2qyxiyDF6TW5Fepq+jVFQzmDCuLzq9wB3jDcAyoQDDsoQGa9ZHOW39aOPW8JZffpJXAsbYK166FWhzeBBVqc7pg4nKUo4a4dr7ilat3e0mprdPQ06gLu2ATE2Yv4OcrWQysZ5IrLbIPiXQKULsBLEBwi5AwIDzkIesuwuIvdfKT1TnaxQipD54ptvAZ0F0pcLy7eCWaR6j40bEgaIenG5i2J/7GhCtqiXNn69CSiUxyrlhfSkYCPgBMR/TVD5E/HTHGHlrIRxCgovoDbiPHBo734dvhgvBC/UMp2Da2HShpbS2pogdfy1ENx6g9MoblBRjTOQJyAen6WgBfBoK90lyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwHwY5ZHcMeCAFdYeYDyKsqgbRM6DNtkLdQSzD8EXWggUR3KkFYFqC9wxKiNBgWfGEp6A0ysUuEnsPbGalnfQPDg==" + } + ] + } + ] +} \ No newline at end of file diff --git a/ironfish/src/rpc/routes/wallet/sendTransaction.test.slow.ts b/ironfish/src/rpc/routes/wallet/sendTransaction.test.slow.ts new file mode 100644 index 0000000000..41a08e230e --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/sendTransaction.test.slow.ts @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { Asset } from '@ironfish/rust-nodejs' +import { Transaction } from '../../../primitives' +import { useAccountFixture, useMinerBlockFixture } from '../../../testUtilities/fixtures' +import { createRouteTest } from '../../../testUtilities/routeTest' +import { AsyncUtils } from '../../../utils' + +describe('Route wallet/sendTransaction (with note selection)', () => { + const routeTest = createRouteTest(true) + + it('spends the specified notes', async () => { + const sender = await useAccountFixture(routeTest.node.wallet, 'accountA') + + const block = await useMinerBlockFixture( + routeTest.chain, + undefined, + sender, + routeTest.node.wallet, + ) + await expect(routeTest.node.chain).toAddBlock(block) + await routeTest.node.wallet.updateHead() + + const decryptedNotes = await AsyncUtils.materialize(sender.getNotes()) + const notes = decryptedNotes.map((note) => note.note.hash().toString('hex')) + expect((await sender.getBalance(Asset.nativeId(), 0)).confirmed).toBe(2000000000n) + + const requestParams = { + account: 'accountA', + outputs: [ + { + publicAddress: sender.publicAddress, + amount: BigInt(10).toString(), + memo: '', + assetId: Asset.nativeId().toString('hex'), + }, + ], + fee: BigInt(1).toString(), + notes, + } + + const response = await routeTest.client.wallet.sendTransaction(requestParams) + + expect(response.status).toBe(200) + expect(response.content.transaction).toBeDefined() + + const transaction = new Transaction(Buffer.from(response.content.transaction, 'hex')) + + expect(transaction.notes.length).toBe(2) + expect(transaction.expiration).toBeDefined() + expect(transaction.burns.length).toBe(0) + expect(transaction.mints.length).toBe(0) + expect(transaction.spends.length).toBe(notes.length) + expect(transaction.fee()).toBe(1n) + + const spendNullifiers = transaction.spends.map((spend) => spend.nullifier.toString('hex')) + + const spends = ( + await routeTest.client.wallet.getNotes({ + account: 'accountA', + }) + ).content.notes.filter((note) => note.nullifier && spendNullifiers.includes(note.nullifier)) + const spendHashes = spends.map((spend) => spend.noteHash) + + expect(new Set(spendHashes)).toEqual(new Set(notes)) + }) +}) diff --git a/ironfish/src/rpc/routes/wallet/sendTransaction.ts b/ironfish/src/rpc/routes/wallet/sendTransaction.ts index b611c6fde4..464e198e01 100644 --- a/ironfish/src/rpc/routes/wallet/sendTransaction.ts +++ b/ironfish/src/rpc/routes/wallet/sendTransaction.ts @@ -27,6 +27,7 @@ export type SendTransactionRequest = { expiration?: number | null expirationDelta?: number | null confirmations?: number | null + notes?: string[] } export type SendTransactionResponse = { @@ -55,6 +56,7 @@ export const SendTransactionRequestSchema: yup.ObjectSchema( } } + if (request.data.notes) { + params.notes = request.data.notes.map((noteHash) => Buffer.from(noteHash, 'hex')) + } + try { const transaction = await context.wallet.send(params) diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index e664a34fcb..8eed7f5f3d 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -876,6 +876,7 @@ export class Wallet { expirationDelta?: number expiration?: number confirmations?: number + notes?: Buffer[] }): Promise { const raw = await this.createTransaction({ account: options.account, @@ -885,6 +886,7 @@ export class Wallet { expirationDelta: options.expirationDelta, expiration: options.expiration ?? undefined, confirmations: options.confirmations ?? undefined, + notes: options.notes, }) const { transaction } = await this.post({ From d39655f684181341d2dbf8b4fe09a94818781106 Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Wed, 17 Jan 2024 14:01:00 -0500 Subject: [PATCH 04/38] Update @typescript-eslint to 6.19.0, eslint to 8.56.0 (#4544) --- config/eslint-config-ironfish/index.js | 1 + config/eslint-config-ironfish/package.json | 6 +- config/eslint-plugin-ironfish/package.json | 2 +- ironfish-cli/src/command.ts | 4 +- ironfish-cli/src/commands/chain/genesisadd.ts | 2 +- .../src/commands/chain/genesisblock.ts | 2 +- ironfish-cli/src/typedefs/blru.d.ts | 2 +- ironfish-cli/src/utils/fees.ts | 2 +- ironfish-cli/src/utils/s3.ts | 2 +- ironfish-cli/src/utils/transaction.ts | 3 +- ironfish/package.json | 6 +- ironfish/src/assert.ts | 2 +- ironfish/src/memPool/memPool.ts | 4 +- ironfish/src/merkletree/merkletree.ts | 2 +- .../network/peers/connections/connection.ts | 4 +- .../src/network/peers/connections/errors.ts | 2 +- .../peers/connections/webSocketConnection.ts | 3 +- .../network/testUtilities/mockLocalPeer.ts | 1 + ironfish/src/primitives/transaction.ts | 4 +- .../rpc/adapters/socketAdapter/protocol.ts | 6 +- .../src/rpc/routes/config/uploadConfig.ts | 3 +- ironfish/src/serde/Serde.ts | 1 - ironfish/src/serde/iJson.ts | 5 +- ironfish/src/storage/database/types.ts | 16 +- ironfish/src/syncer.ts | 2 +- ironfish/src/testUtilities/fixtures/blocks.ts | 18 +- ironfish/src/testUtilities/nodeTest.ts | 3 +- ironfish/src/typedefs/blru.d.ts | 2 +- ironfish/src/utils/retry.ts | 4 +- ironfish/src/wallet/walletdb/walletdb.ts | 6 +- package.json | 6 +- yarn.lock | 466 +++++++++--------- 32 files changed, 284 insertions(+), 308 deletions(-) diff --git a/config/eslint-config-ironfish/index.js b/config/eslint-config-ironfish/index.js index 27d53ed658..978bcb75d7 100644 --- a/config/eslint-config-ironfish/index.js +++ b/config/eslint-config-ironfish/index.js @@ -45,6 +45,7 @@ module.exports = { // the matchers to unknown, or defining a custom matcher, which seems // like too much friction for test-writing. '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', // It's common to want to mock unbound methods. '@typescript-eslint/unbound-method': 'off', // Using try catch with expect.assertions(n) is the recommended way to diff --git a/config/eslint-config-ironfish/package.json b/config/eslint-config-ironfish/package.json index 6166b45e1f..1a0e70fb81 100644 --- a/config/eslint-config-ironfish/package.json +++ b/config/eslint-config-ironfish/package.json @@ -9,9 +9,9 @@ ".prettierrc" ], "peerDependencies": { - "@typescript-eslint/eslint-plugin": "4.28.1", - "@typescript-eslint/parser": "4.28.1", - "eslint": "7.29.0", + "@typescript-eslint/eslint-plugin": "6.19.0", + "@typescript-eslint/parser": "6.19.0", + "eslint": "8.56.0", "eslint-config-prettier": "8.3.0", "eslint-plugin-ironfish": "*", "eslint-plugin-jest": "27.1.6", diff --git a/config/eslint-plugin-ironfish/package.json b/config/eslint-plugin-ironfish/package.json index d979e55ac1..9c39bff985 100644 --- a/config/eslint-plugin-ironfish/package.json +++ b/config/eslint-plugin-ironfish/package.json @@ -8,7 +8,7 @@ "index.js" ], "peerDependencies": { - "eslint": "7.29.0" + "eslint": "8.56.0" }, "scripts": { "test": "node index.test.js" diff --git a/ironfish-cli/src/command.ts b/ironfish-cli/src/command.ts index e11bd651c2..5459dbaefe 100644 --- a/ironfish-cli/src/command.ts +++ b/ironfish-cli/src/command.ts @@ -111,7 +111,7 @@ export abstract class IronfishCommand extends Command { async init(): Promise { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any const commandClass = this.constructor as any - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument const { flags } = await this.parse(commandClass) // Get the flags from the flag object which is unknown @@ -218,7 +218,7 @@ export abstract class IronfishCommand extends Command { } } -function getFlag(flags: unknown, flag: FLAGS): unknown | null { +function getFlag(flags: unknown, flag: FLAGS): unknown { return typeof flags === 'object' && flags !== null && flag in flags ? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any (flags as any)[flag] diff --git a/ironfish-cli/src/commands/chain/genesisadd.ts b/ironfish-cli/src/commands/chain/genesisadd.ts index 477fae6281..cd86541cd1 100644 --- a/ironfish-cli/src/commands/chain/genesisadd.ts +++ b/ironfish-cli/src/commands/chain/genesisadd.ts @@ -97,7 +97,7 @@ export default class GenesisAddCommand extends IronfishCommand { } CliUx.ux.table(allocations, columns, { - printLine: (line) => this.log(line), + printLine: this.log.bind(this), }) // Display duplicates if they exist diff --git a/ironfish-cli/src/commands/chain/genesisblock.ts b/ironfish-cli/src/commands/chain/genesisblock.ts index 283996cf25..0eb47581ba 100644 --- a/ironfish-cli/src/commands/chain/genesisblock.ts +++ b/ironfish-cli/src/commands/chain/genesisblock.ts @@ -152,7 +152,7 @@ export default class GenesisBlockCommand extends IronfishCommand { } CliUx.ux.table(info.allocations, columns, { - printLine: (line) => this.log(line), + printLine: this.log.bind(this), }) // Display duplicates if they exist diff --git a/ironfish-cli/src/typedefs/blru.d.ts b/ironfish-cli/src/typedefs/blru.d.ts index 721b3ee663..6133b257bf 100644 --- a/ironfish-cli/src/typedefs/blru.d.ts +++ b/ironfish-cli/src/typedefs/blru.d.ts @@ -9,7 +9,7 @@ declare module 'blru' { constructor( capacity: number, getSize?: GetSizeFunction | null, - CustomMap?: typeof Map | unknown | null, + CustomMap?: unknown, ) map: Map> diff --git a/ironfish-cli/src/utils/fees.ts b/ironfish-cli/src/utils/fees.ts index d281e82b3f..83070ee842 100644 --- a/ironfish-cli/src/utils/fees.ts +++ b/ironfish-cli/src/utils/fees.ts @@ -109,7 +109,7 @@ async function getTxWithFee( }) const response = await promise.catch((e) => { - if (e instanceof RpcRequestError && e.code === ERROR_CODES.INSUFFICIENT_BALANCE) { + if (e instanceof RpcRequestError && e.code === ERROR_CODES.INSUFFICIENT_BALANCE.valueOf()) { return null } else { throw e diff --git a/ironfish-cli/src/utils/s3.ts b/ironfish-cli/src/utils/s3.ts index f7af8fd1d1..af30e5e87a 100644 --- a/ironfish-cli/src/utils/s3.ts +++ b/ironfish-cli/src/utils/s3.ts @@ -32,7 +32,7 @@ const MAX_MULTIPART_NUM = 10000 class UploadToBucketError extends Error { name = this.constructor.name - error: unknown | undefined + error: unknown constructor(message?: string, error?: unknown) { super(message) diff --git a/ironfish-cli/src/utils/transaction.ts b/ironfish-cli/src/utils/transaction.ts index a335e7d27a..cac333906c 100644 --- a/ironfish-cli/src/utils/transaction.ts +++ b/ironfish-cli/src/utils/transaction.ts @@ -65,7 +65,8 @@ export async function watchTransaction(options: { const startTime = lastTime - let prevStatus = last?.content.transaction?.status ?? 'not found' + let prevStatus: TransactionStatus | 'not found' = + last?.content.transaction?.status ?? 'not found' let currentStatus = prevStatus // If the transaction is already in the desired state, return diff --git a/ironfish/package.json b/ironfish/package.json index 89ef24c138..3fb843e770 100644 --- a/ironfish/package.json +++ b/ironfish/package.json @@ -80,10 +80,10 @@ "@types/uuid": "^8.0.1", "@types/ws": "8.5.4", "@types/yup": "0.29.10", - "@typescript-eslint/eslint-plugin": "4.28.1", - "@typescript-eslint/parser": "4.28.1", + "@typescript-eslint/eslint-plugin": "6.19.0", + "@typescript-eslint/parser": "6.19.0", "cross-env": "7.0.3", - "eslint": "7.29.0", + "eslint": "8.56.0", "eslint-config-ironfish": "*", "eslint-config-prettier": "8.3.0", "eslint-plugin-jest": "27.1.6", diff --git a/ironfish/src/assert.ts b/ironfish/src/assert.ts index 9d997298fa..2ede467a63 100644 --- a/ironfish/src/assert.ts +++ b/ironfish/src/assert.ts @@ -77,7 +77,7 @@ export class Assert { } } - static isTruthy( + static isTruthy( x: T, message?: string, ): asserts x is Exclude { diff --git a/ironfish/src/memPool/memPool.ts b/ironfish/src/memPool/memPool.ts index 5230a26427..c732d7ba3c 100644 --- a/ironfish/src/memPool/memPool.ts +++ b/ironfish/src/memPool/memPool.ts @@ -11,7 +11,7 @@ import { createRootLogger, Logger } from '../logger' import { MetricsMonitor } from '../metrics' import { getTransactionSize } from '../network/utils/serializers' import { Block, BlockHeader } from '../primitives' -import { Transaction, TransactionHash } from '../primitives/transaction' +import { Transaction, TransactionHash, TransactionVersion } from '../primitives/transaction' import { PriorityQueue } from '../utils' import { FeeEstimator, getPreciseFeeRate } from './feeEstimator' import { RecentlyEvictedCache } from './recentlyEvictedCache' @@ -27,7 +27,7 @@ interface ExpirationMempoolEntry { } interface VersionMempoolEntry { - version: number + version: TransactionVersion hash: TransactionHash } diff --git a/ironfish/src/merkletree/merkletree.ts b/ironfish/src/merkletree/merkletree.ts index a1da1adbee..a6b0f740b7 100644 --- a/ironfish/src/merkletree/merkletree.ts +++ b/ironfish/src/merkletree/merkletree.ts @@ -882,7 +882,7 @@ export class MerkleTree< newHash = this.hasher.combineHash(element.depth + 1, element.hash, element.hash) } else { newHash = - node.side === 'Left' + node.side === Side.Left ? this.hasher.combineHash(element.depth + 1, element.hash, node.hashOfSibling) : this.hasher.combineHash(element.depth + 1, node.hashOfSibling, element.hash) } diff --git a/ironfish/src/network/peers/connections/connection.ts b/ironfish/src/network/peers/connections/connection.ts index ba01974b52..e4bac18836 100644 --- a/ironfish/src/network/peers/connections/connection.ts +++ b/ironfish/src/network/peers/connections/connection.ts @@ -52,8 +52,8 @@ export abstract class Connection { /** * The last error received (if any), regardless of the current state of the connection. */ - protected _error: unknown | null - get error(): Readonly | null { + protected _error: unknown + get error(): Readonly { return this._error as Readonly } diff --git a/ironfish/src/network/peers/connections/errors.ts b/ironfish/src/network/peers/connections/errors.ts index 20e1f65683..9e7c131530 100644 --- a/ironfish/src/network/peers/connections/errors.ts +++ b/ironfish/src/network/peers/connections/errors.ts @@ -6,7 +6,7 @@ import { ErrorUtils } from '../../../utils' export class NetworkError extends Error { name = this.constructor.name - wrappedError: unknown | null + wrappedError: unknown constructor(message?: string, wrappedError?: unknown) { super(ErrorUtils.renderError(message || wrappedError || 'Unknown Network Error')) diff --git a/ironfish/src/network/peers/connections/webSocketConnection.ts b/ironfish/src/network/peers/connections/webSocketConnection.ts index b0806c892b..a736a8fe1f 100644 --- a/ironfish/src/network/peers/connections/webSocketConnection.ts +++ b/ironfish/src/network/peers/connections/webSocketConnection.ts @@ -5,6 +5,7 @@ import type { Logger } from '../../../logger' import colors from 'colors/safe' import { MetricsMonitor } from '../../../metrics' +import { IJSON } from '../../../serde/iJson' import { parseNetworkMessage } from '../../messageRegistry' import { IsomorphicWebSocket, IsomorphicWebSocketErrorEvent } from '../../types' import { WebSocketAddress } from '../../utils' @@ -71,7 +72,7 @@ export class WebSocketConnection extends Connection { this.socket.onmessage = (event: MessageEvent) => { if (!Buffer.isBuffer(event.data)) { const message = 'Received non-buffer message' - this.logger.debug(message, event.data) + this.logger.debug(`${message}: ${IJSON.stringify(event.data)}`) this.close(new NetworkError(message)) return } diff --git a/ironfish/src/network/testUtilities/mockLocalPeer.ts b/ironfish/src/network/testUtilities/mockLocalPeer.ts index 96ff2664e9..f0bac75c02 100644 --- a/ironfish/src/network/testUtilities/mockLocalPeer.ts +++ b/ironfish/src/network/testUtilities/mockLocalPeer.ts @@ -49,6 +49,7 @@ export function mockLocalPeer({ identity, agent, version, + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument chain || mockChain(), WebSocketClient, mockNodeDataChannel, diff --git a/ironfish/src/primitives/transaction.ts b/ironfish/src/primitives/transaction.ts index 6e6f971172..4f3c135796 100644 --- a/ironfish/src/primitives/transaction.ts +++ b/ironfish/src/primitives/transaction.ts @@ -68,7 +68,7 @@ export class Transaction { const reader = bufio.read(this.transactionPostedSerialized, true) this._version = reader.readU8() // 1 - if (this._version < 1 || this._version > 2) { + if (this._version < TransactionVersion.V1 || this._version > TransactionVersion.V2) { throw new UnsupportedVersionError(this._version) } const _spendsLength = reader.readU64() // 8 @@ -188,7 +188,7 @@ export class Transaction { const result = callback(transaction) - Promise.resolve(result).finally(() => { + void Promise.resolve(result).finally(() => { this.returnReference() }) diff --git a/ironfish/src/rpc/adapters/socketAdapter/protocol.ts b/ironfish/src/rpc/adapters/socketAdapter/protocol.ts index 1fac26c870..5335facb84 100644 --- a/ironfish/src/rpc/adapters/socketAdapter/protocol.ts +++ b/ironfish/src/rpc/adapters/socketAdapter/protocol.ts @@ -19,18 +19,18 @@ export type SocketRpcRequest = { mid: number type: string auth: string | null | undefined - data: unknown | undefined + data: unknown } export type SocketRpcResponse = { id: number status: number - data: unknown | undefined + data: unknown } export type SocketRpcStream = { id: number - data: unknown | undefined + data: unknown } export type SocketRpcError = { diff --git a/ironfish/src/rpc/routes/config/uploadConfig.ts b/ironfish/src/rpc/routes/config/uploadConfig.ts index bf6cbbd5cb..5c6a64ba36 100644 --- a/ironfish/src/rpc/routes/config/uploadConfig.ts +++ b/ironfish/src/rpc/routes/config/uploadConfig.ts @@ -81,8 +81,7 @@ export function setUnknownConfigValue( value = convertValue(sourceKey, sourceValue, targetValue) } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - config.set(sourceKey, value as any) + config.set(sourceKey, value as never) } // Expects string in CSV format with no brackets diff --git a/ironfish/src/serde/Serde.ts b/ironfish/src/serde/Serde.ts index 9fe8a1fa37..57d07100f8 100644 --- a/ironfish/src/serde/Serde.ts +++ b/ironfish/src/serde/Serde.ts @@ -20,7 +20,6 @@ export type IJsonSerializable = | Buffer | IJsonSerializable[] | { [key: string]: IJsonSerializable } - | unknown /** * Interface for objects that can be serialized, deserialized, and compared for equality. diff --git a/ironfish/src/serde/iJson.ts b/ironfish/src/serde/iJson.ts index 23b2cfc177..90f063aaed 100644 --- a/ironfish/src/serde/iJson.ts +++ b/ironfish/src/serde/iJson.ts @@ -14,7 +14,10 @@ export const IJSON = { (key, value) => typeof value === 'bigint' ? `${value.toString()}n` - : (BJSON.replacer(key, value) as unknown), + : (BJSON.replacer( + key, + value as string /* The buffer-json types are wrong, this shouldn't need 'as string' */, + ) as unknown), space, ) }, diff --git a/ironfish/src/storage/database/types.ts b/ironfish/src/storage/database/types.ts index fe24949e95..474af67c0b 100644 --- a/ironfish/src/storage/database/types.ts +++ b/ironfish/src/storage/database/types.ts @@ -17,19 +17,9 @@ export interface DatabaseIteratorOptions { ordered?: boolean } -export type DatabaseKey = - | bigint - | number - | string - | Date - | Buffer - | Array - | unknown - -export type DatabaseSchema< - key extends DatabaseKey = DatabaseKey, - value extends unknown = unknown, -> = { +export type DatabaseKey = bigint | number | string | Date | Buffer | Array + +export type DatabaseSchema = { key: key value: value } diff --git a/ironfish/src/syncer.ts b/ironfish/src/syncer.ts index cd69647793..8c59c6ae8b 100644 --- a/ironfish/src/syncer.ts +++ b/ironfish/src/syncer.ts @@ -291,7 +291,7 @@ export class Syncer { peer.onStateChanged.on(this.onPeerStateChanged) this.stopping = this.syncFrom(peer) - .catch((error) => { + .catch((error: Readonly) => { if (error instanceof AbortSyncingError || this.loader !== peer) { return } diff --git a/ironfish/src/testUtilities/fixtures/blocks.ts b/ironfish/src/testUtilities/fixtures/blocks.ts index 1e8de7e92f..517d288e16 100644 --- a/ironfish/src/testUtilities/fixtures/blocks.ts +++ b/ironfish/src/testUtilities/fixtures/blocks.ts @@ -289,7 +289,7 @@ export async function useBlockWithCustomTxs( fee?: bigint to?: SpendingAccount from: SpendingAccount - outputs: TransactionOutput[] + outputs?: TransactionOutput[] }[], ): Promise<{ block: Block @@ -354,9 +354,9 @@ export async function useBlockWithCustomTxs( export async function useBlockWithTxs( node: FullNode, numTransactions: number, - from?: Account, + from?: SpendingAccount, ): Promise<{ - account: Account + account: SpendingAccount block: Block transactions: Transaction[] }> { @@ -364,10 +364,14 @@ export async function useBlockWithTxs( from = await useAccountFixture(node.wallet, 'test') } - const { block, transactions } = await useBlockWithCustomTxs( - node, - Array(numTransactions).fill({ from }), - ) + const transactionInputs = new Array<{ + fee?: bigint + to?: SpendingAccount + from: SpendingAccount + outputs?: TransactionOutput[] + }>(numTransactions).fill({ from }) + + const { block, transactions } = await useBlockWithCustomTxs(node, transactionInputs) return { block, transactions, account: from } } diff --git a/ironfish/src/testUtilities/nodeTest.ts b/ironfish/src/testUtilities/nodeTest.ts index 18140ef8b4..0752a06b03 100644 --- a/ironfish/src/testUtilities/nodeTest.ts +++ b/ironfish/src/testUtilities/nodeTest.ts @@ -86,8 +86,7 @@ export class NodeTest { for (const key in options.config) { const configKey = key as keyof ConfigOptions const configValue = options.config[configKey] - // eslint-disable-next-line @typescript-eslint/no-explicit-any - sdk.config.setOverride(key as keyof ConfigOptions, configValue as any) + sdk.config.setOverride(key as keyof ConfigOptions, configValue) } } diff --git a/ironfish/src/typedefs/blru.d.ts b/ironfish/src/typedefs/blru.d.ts index 721b3ee663..6133b257bf 100644 --- a/ironfish/src/typedefs/blru.d.ts +++ b/ironfish/src/typedefs/blru.d.ts @@ -9,7 +9,7 @@ declare module 'blru' { constructor( capacity: number, getSize?: GetSizeFunction | null, - CustomMap?: typeof Map | unknown | null, + CustomMap?: unknown, ) map: Map> diff --git a/ironfish/src/utils/retry.ts b/ironfish/src/utils/retry.ts index 8e60f78dbd..c42aeb4e28 100644 --- a/ironfish/src/utils/retry.ts +++ b/ironfish/src/utils/retry.ts @@ -25,10 +25,10 @@ export class Retry { }) } - private tryExecutor( + private tryExecutor( fn: () => Promise, resolve: (result: T) => void, - reject: (error: E) => void, + reject: (error: unknown) => void, ) { fn() .then((result) => { diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index b22d3d4db3..44dbb2138b 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -15,6 +15,7 @@ import { BUFFER_ENCODING, BufferEncoding, DatabaseKeyRange, + DatabaseSchema, IDatabase, IDatabaseStore, IDatabaseTransaction, @@ -126,10 +127,7 @@ export class WalletDB { value: null }> - cacheStores: IDatabaseStore<{ - key: Readonly - value: unknown - }>[] + cacheStores: Array> constructor({ files, diff --git a/package.json b/package.json index a91156b948..1fdc783105 100644 --- a/package.json +++ b/package.json @@ -37,10 +37,10 @@ }, "devDependencies": { "@types/jest": "29.5.8", - "@typescript-eslint/eslint-plugin": "4.28.1", - "@typescript-eslint/parser": "4.28.1", + "@typescript-eslint/eslint-plugin": "6.19.0", + "@typescript-eslint/parser": "6.19.0", "codecov": "3.8.3", - "eslint": "7.29.0", + "eslint": "8.56.0", "eslint-config-prettier": "8.3.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-import": "2.23.4", diff --git a/yarn.lock b/yarn.lock index 3c6dc20988..ecb7c2d652 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1075,13 +1075,6 @@ dependencies: tslib "^2.5.0" -"@babel/code-frame@7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" - integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== - dependencies: - "@babel/highlight" "^7.10.4" - "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.22.13": version "7.22.13" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" @@ -1218,7 +1211,7 @@ "@babel/traverse" "^7.23.2" "@babel/types" "^7.23.0" -"@babel/highlight@^7.10.4", "@babel/highlight@^7.22.13": +"@babel/highlight@^7.22.13": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== @@ -1383,28 +1376,38 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@eslint-community/eslint-utils@^4.4.0": +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== dependencies: eslint-visitor-keys "^3.3.0" -"@eslint/eslintrc@^0.4.2": - version "0.4.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" - integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== +"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== + +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== dependencies: ajv "^6.12.4" - debug "^4.1.1" - espree "^7.3.0" - globals "^13.9.0" - ignore "^4.0.6" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" import-fresh "^3.2.1" - js-yaml "^3.13.1" - minimatch "^3.0.4" + js-yaml "^4.1.0" + minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@eslint/js@8.56.0": + version "8.56.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b" + integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A== + "@ethersproject/bignumber@5.7.0": version "5.7.0" resolved "https://registry.npmmirror.com/@ethersproject/bignumber/-/bignumber-5.7.0.tgz#e2f03837f268ba655ffba03a57853e18a18dc9c2" @@ -1448,6 +1451,25 @@ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== +"@humanwhocodes/config-array@^0.11.13": + version "0.11.14" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" + integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== + dependencies: + "@humanwhocodes/object-schema" "^2.0.2" + debug "^4.3.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" + integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== + "@hutson/parse-repository-url@^3.0.0": version "3.0.2" resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" @@ -2630,7 +2652,7 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== -"@nodelib/fs.walk@^1.2.3": +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": version "1.2.8" resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== @@ -3563,11 +3585,6 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85" integrity sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ== -"@types/json-schema@^7.0.7": - version "7.0.15" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" - integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== - "@types/json-schema@^7.0.9": version "7.0.9" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" @@ -3734,48 +3751,33 @@ resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.29.10.tgz#1bfa4c4a47a6f57fcc8510948757b9e47c0d6ca3" integrity sha512-kRKRZaWkxxnOK7H5C4oWqhCw9ID1QF3cBZ2oAPoXYsjIncwgpDGigWtXGjZ91t+hsc3cvPdBci9YoJo1A96CYg== -"@typescript-eslint/eslint-plugin@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.1.tgz#c045e440196ae45464e08e20c38aff5c3a825947" - integrity sha512-9yfcNpDaNGQ6/LQOX/KhUFTR1sCKH+PBr234k6hI9XJ0VP5UqGxap0AnNwBnWFk1MNyWBylJH9ZkzBXC+5akZQ== - dependencies: - "@typescript-eslint/experimental-utils" "4.28.1" - "@typescript-eslint/scope-manager" "4.28.1" - debug "^4.3.1" - functional-red-black-tree "^1.0.1" - regexpp "^3.1.0" - semver "^7.3.5" - tsutils "^3.21.0" - -"@typescript-eslint/experimental-utils@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.1.tgz#3869489dcca3c18523c46018b8996e15948dbadc" - integrity sha512-n8/ggadrZ+uyrfrSEchx3jgODdmcx7MzVM2sI3cTpI/YlfSm0+9HEUaWw3aQn2urL2KYlWYMDgn45iLfjDYB+Q== - dependencies: - "@types/json-schema" "^7.0.7" - "@typescript-eslint/scope-manager" "4.28.1" - "@typescript-eslint/types" "4.28.1" - "@typescript-eslint/typescript-estree" "4.28.1" - eslint-scope "^5.1.1" - eslint-utils "^3.0.0" - -"@typescript-eslint/parser@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.1.tgz#5181b81658414f47291452c15bf6cd44a32f85bd" - integrity sha512-UjrMsgnhQIIK82hXGaD+MCN8IfORS1CbMdu7VlZbYa8LCZtbZjJA26De4IPQB7XYZbL8gJ99KWNj0l6WD0guJg== +"@typescript-eslint/eslint-plugin@6.19.0": + version "6.19.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.19.0.tgz#db03f3313b57a30fbbdad2e6929e88fc7feaf9ba" + integrity sha512-DUCUkQNklCQYnrBSSikjVChdc84/vMPDQSgJTHBZ64G9bA9w0Crc0rd2diujKbTdp6w2J47qkeHQLoi0rpLCdg== dependencies: - "@typescript-eslint/scope-manager" "4.28.1" - "@typescript-eslint/types" "4.28.1" - "@typescript-eslint/typescript-estree" "4.28.1" - debug "^4.3.1" + "@eslint-community/regexpp" "^4.5.1" + "@typescript-eslint/scope-manager" "6.19.0" + "@typescript-eslint/type-utils" "6.19.0" + "@typescript-eslint/utils" "6.19.0" + "@typescript-eslint/visitor-keys" "6.19.0" + debug "^4.3.4" + graphemer "^1.4.0" + ignore "^5.2.4" + natural-compare "^1.4.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" -"@typescript-eslint/scope-manager@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.1.tgz#fd3c20627cdc12933f6d98b386940d8d0ce8a991" - integrity sha512-o95bvGKfss6705x7jFGDyS7trAORTy57lwJ+VsYwil/lOUxKQ9tA7Suuq+ciMhJc/1qPwB3XE2DKh9wubW8YYA== +"@typescript-eslint/parser@6.19.0": + version "6.19.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.19.0.tgz#80344086f362181890ade7e94fc35fe0480bfdf5" + integrity sha512-1DyBLG5SH7PYCd00QlroiW60YJ4rWMuUGa/JBV0iZuqi4l4IK3twKPq5ZkEebmGqRjXWVgsUzfd3+nZveewgow== dependencies: - "@typescript-eslint/types" "4.28.1" - "@typescript-eslint/visitor-keys" "4.28.1" + "@typescript-eslint/scope-manager" "6.19.0" + "@typescript-eslint/types" "6.19.0" + "@typescript-eslint/typescript-estree" "6.19.0" + "@typescript-eslint/visitor-keys" "6.19.0" + debug "^4.3.4" "@typescript-eslint/scope-manager@5.14.0": version "5.14.0" @@ -3793,6 +3795,14 @@ "@typescript-eslint/types" "5.46.0" "@typescript-eslint/visitor-keys" "5.46.0" +"@typescript-eslint/scope-manager@6.19.0": + version "6.19.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.19.0.tgz#b6d2abb825b29ab70cb542d220e40c61c1678116" + integrity sha512-dO1XMhV2ehBI6QN8Ufi7I10wmUovmLU0Oru3n5LVlM2JuzB4M+dVphCPLkVpKvGij2j/pHBWuJ9piuXx+BhzxQ== + dependencies: + "@typescript-eslint/types" "6.19.0" + "@typescript-eslint/visitor-keys" "6.19.0" + "@typescript-eslint/scope-manager@6.7.2": version "6.7.2" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.7.2.tgz#cf59a2095d2f894770c94be489648ad1c78dc689" @@ -3801,10 +3811,15 @@ "@typescript-eslint/types" "6.7.2" "@typescript-eslint/visitor-keys" "6.7.2" -"@typescript-eslint/types@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.1.tgz#d0f2ecbef3684634db357b9bbfc97b94b828f83f" - integrity sha512-4z+knEihcyX7blAGi7O3Fm3O6YRCP+r56NJFMNGsmtdw+NCdpG5SgNz427LS9nQkRVTswZLhz484hakQwB8RRg== +"@typescript-eslint/type-utils@6.19.0": + version "6.19.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.19.0.tgz#522a494ef0d3e9fdc5e23a7c22c9331bbade0101" + integrity sha512-mcvS6WSWbjiSxKCwBcXtOM5pRkPQ6kcDds/juxcy/727IQr3xMEcwr/YLHW2A2+Fp5ql6khjbKBzOyjuPqGi/w== + dependencies: + "@typescript-eslint/typescript-estree" "6.19.0" + "@typescript-eslint/utils" "6.19.0" + debug "^4.3.4" + ts-api-utils "^1.0.1" "@typescript-eslint/types@5.14.0": version "5.14.0" @@ -3816,24 +3831,16 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.46.0.tgz#f4d76622a996b88153bbd829ea9ccb9f7a5d28bc" integrity sha512-wHWgQHFB+qh6bu0IAPAJCdeCdI0wwzZnnWThlmHNY01XJ9Z97oKqKOzWYpR2I83QmshhQJl6LDM9TqMiMwJBTw== +"@typescript-eslint/types@6.19.0": + version "6.19.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.19.0.tgz#689b0498c436272a6a2059b09f44bcbd90de294a" + integrity sha512-lFviGV/vYhOy3m8BJ/nAKoAyNhInTdXpftonhWle66XHAtT1ouBlkjL496b5H5hb8dWXHwtypTqgtb/DEa+j5A== + "@typescript-eslint/types@6.7.2": version "6.7.2" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.7.2.tgz#75a615a6dbeca09cafd102fe7f465da1d8a3c066" integrity sha512-flJYwMYgnUNDAN9/GAI3l8+wTmvTYdv64fcH8aoJK76Y+1FCZ08RtI5zDerM/FYT5DMkAc+19E4aLmd5KqdFyg== -"@typescript-eslint/typescript-estree@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.1.tgz#af882ae41740d1f268e38b4d0fad21e7e8d86a81" - integrity sha512-GhKxmC4sHXxHGJv8e8egAZeTZ6HI4mLU6S7FUzvFOtsk7ZIDN1ksA9r9DyOgNqowA9yAtZXV0Uiap61bIO81FQ== - dependencies: - "@typescript-eslint/types" "4.28.1" - "@typescript-eslint/visitor-keys" "4.28.1" - debug "^4.3.1" - globby "^11.0.3" - is-glob "^4.0.1" - semver "^7.3.5" - tsutils "^3.21.0" - "@typescript-eslint/typescript-estree@5.14.0": version "5.14.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.14.0.tgz#78b7f7385d5b6f2748aacea5c9b7f6ae62058314" @@ -3860,6 +3867,20 @@ semver "^7.3.7" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@6.19.0": + version "6.19.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.0.tgz#0813ba364a409afb4d62348aec0202600cb468fa" + integrity sha512-o/zefXIbbLBZ8YJ51NlkSAt2BamrK6XOmuxSR3hynMIzzyMY33KuJ9vuMdFSXW+H0tVvdF9qBPTHA91HDb4BIQ== + dependencies: + "@typescript-eslint/types" "6.19.0" + "@typescript-eslint/visitor-keys" "6.19.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "9.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + "@typescript-eslint/typescript-estree@6.7.2": version "6.7.2" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.2.tgz#ce5883c23b581a5caf878af641e49dd0349238c7" @@ -3885,6 +3906,19 @@ eslint-scope "^5.1.1" eslint-utils "^3.0.0" +"@typescript-eslint/utils@6.19.0": + version "6.19.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.19.0.tgz#557b72c3eeb4f73bef8037c85dae57b21beb1a4b" + integrity sha512-QR41YXySiuN++/dC9UArYOg4X86OAYP83OWTewpVx5ct1IZhjjgTLocj7QNxGhWoTqknsgpl7L+hGygCO+sdYw== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.19.0" + "@typescript-eslint/types" "6.19.0" + "@typescript-eslint/typescript-estree" "6.19.0" + semver "^7.5.4" + "@typescript-eslint/utils@^5.10.0": version "5.46.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.46.0.tgz#600cd873ba471b7d8b0b9f35de34cf852c6fcb31" @@ -3912,14 +3946,6 @@ "@typescript-eslint/typescript-estree" "6.7.2" semver "^7.5.4" -"@typescript-eslint/visitor-keys@4.28.1": - version "4.28.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.1.tgz#162a515ee255f18a6068edc26df793cdc1ec9157" - integrity sha512-K4HMrdFqr9PFquPu178SaSb92CaWe2yErXyPumc8cYWxFmhgJsNY9eSePmO05j0JhBvf2Cdhptd6E6Yv9HVHcg== - dependencies: - "@typescript-eslint/types" "4.28.1" - eslint-visitor-keys "^2.0.0" - "@typescript-eslint/visitor-keys@5.14.0": version "5.14.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.14.0.tgz#1927005b3434ccd0d3ae1b2ecf60e65943c36986" @@ -3936,6 +3962,14 @@ "@typescript-eslint/types" "5.46.0" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@6.19.0": + version "6.19.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.0.tgz#4565e0ecd63ca1f81b96f1dd76e49f746c6b2b49" + integrity sha512-hZaUCORLgubBvtGpp1JEFEazcuEdfxta9j4iUwdSAr7mEsYYAp3EAUyCZk3VEEqGj6W+AV4uWyrDGtrlawAsgQ== + dependencies: + "@typescript-eslint/types" "6.19.0" + eslint-visitor-keys "^3.4.1" + "@typescript-eslint/visitor-keys@6.7.2": version "6.7.2" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.2.tgz#4cb2bd786f1f459731b0ad1584c9f73e1c7a4d5c" @@ -3944,6 +3978,11 @@ "@typescript-eslint/types" "6.7.2" eslint-visitor-keys "^3.4.1" +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + "@yarnpkg/lockfile@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" @@ -3988,7 +4027,7 @@ abstract-leveldown@~6.2.1: level-supports "~1.0.0" xtend "~4.0.0" -acorn-jsx@^5.3.1: +acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== @@ -3998,16 +4037,16 @@ acorn-walk@^8.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -acorn@^7.4.0: - version "7.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" - integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== - acorn@^8.4.1: version "8.8.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== +acorn@^8.9.0: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + add-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" @@ -4046,7 +4085,7 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" -ajv@^6.10.0, ajv@^6.12.4: +ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -4056,16 +4095,6 @@ ajv@^6.10.0, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.1: - version "8.12.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" - integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== - dependencies: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" - ansi-colors@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" @@ -4246,11 +4275,6 @@ assertion-error@^1.1.0: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== - async@^3.2.3: version "3.2.4" resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" @@ -5194,13 +5218,6 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.0.1, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - debug@^4.3.2, debug@^4.3.3: version "4.3.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" @@ -5208,6 +5225,13 @@ debug@^4.3.2, debug@^4.3.3: dependencies: ms "2.1.2" +debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -5446,14 +5470,6 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" -enquirer@^2.3.5: - version "2.4.1" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.4.1.tgz#93334b3fbd74fc7097b224ab4a8fb7e40bf4ae56" - integrity sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ== - dependencies: - ansi-colors "^4.1.1" - strip-ansi "^6.0.1" - enquirer@~2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" @@ -5634,12 +5650,13 @@ eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== dependencies: - eslint-visitor-keys "^1.1.0" + esrecurse "^4.3.0" + estraverse "^5.2.0" eslint-utils@^3.0.0: version "3.0.0" @@ -5648,17 +5665,12 @@ eslint-utils@^3.0.0: dependencies: eslint-visitor-keys "^2.0.0" -eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - eslint-visitor-keys@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== -eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.4.1: +eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== @@ -5668,59 +5680,58 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@7.29.0: - version "7.29.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.29.0.tgz#ee2a7648f2e729485e4d0bd6383ec1deabc8b3c0" - integrity sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA== - dependencies: - "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.2" - ajv "^6.10.0" +eslint@8.56.0: + version "8.56.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.56.0.tgz#4957ce8da409dc0809f99ab07a1b94832ab74b15" + integrity sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.56.0" + "@humanwhocodes/config-array" "^0.11.13" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" - debug "^4.0.1" + debug "^4.3.2" doctrine "^3.0.0" - enquirer "^2.3.5" escape-string-regexp "^4.0.0" - eslint-scope "^5.1.1" - eslint-utils "^2.1.0" - eslint-visitor-keys "^2.0.0" - espree "^7.3.1" - esquery "^1.4.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^5.1.2" - globals "^13.6.0" - ignore "^4.0.6" - import-fresh "^3.0.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" imurmurhash "^0.1.4" is-glob "^4.0.0" - js-yaml "^3.13.1" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" - minimatch "^3.0.4" + minimatch "^3.1.2" natural-compare "^1.4.0" - optionator "^0.9.1" - progress "^2.0.0" - regexpp "^3.1.0" - semver "^7.2.1" - strip-ansi "^6.0.0" - strip-json-comments "^3.1.0" - table "^6.0.9" + optionator "^0.9.3" + strip-ansi "^6.0.1" text-table "^0.2.0" - v8-compile-cache "^2.0.3" -espree@^7.3.0, espree@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" - integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== dependencies: - acorn "^7.4.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^1.3.0" + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" esprima@^4.0.0, esprima@~4.0.0: version "4.0.1" @@ -5734,7 +5745,7 @@ esquery@^1.0.1: dependencies: estraverse "^5.1.0" -esquery@^1.4.0: +esquery@^1.4.2: version "1.5.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== @@ -6143,11 +6154,6 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== - gauge@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" @@ -6353,6 +6359,13 @@ glob-parent@^5.1.1, glob-parent@^5.1.2: dependencies: is-glob "^4.0.1" +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + glob@7.1.4: version "7.1.4" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" @@ -6393,10 +6406,10 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.6.0, globals@^13.9.0: - version "13.23.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.23.0.tgz#ef31673c926a0976e1f61dab4dca57e0c0a8af02" - integrity sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA== +globals@^13.19.0: + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== dependencies: type-fest "^0.20.2" @@ -6453,6 +6466,11 @@ graceful-fs@^4.2.9: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + grouped-queue@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/grouped-queue/-/grouped-queue-2.0.0.tgz#a2c6713f2171e45db2c300a3a9d7c119d694dac8" @@ -6667,11 +6685,6 @@ ignore-walk@^5.0.1: dependencies: minimatch "^5.0.1" -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - ignore@^5.0.4, ignore@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" @@ -6682,12 +6695,17 @@ ignore@^5.1.1, ignore@^5.1.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.9.tgz#9ec1a5cbe8e1446ec60d4420060d43aa6e7382fb" integrity sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ== +ignore@^5.2.4: + version "5.3.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" + integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== + immediate@^3.2.3: version "3.3.0" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.3.0.tgz#1aef225517836bcdf7f2a2de2600c79ff0269266" integrity sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q== -import-fresh@^3.0.0, import-fresh@^3.2.1: +import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -6918,6 +6936,11 @@ is-obj@^2.0.0: resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" @@ -7646,11 +7669,6 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== - json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" @@ -7954,11 +7972,6 @@ lodash.set@^4.3.2: resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= -lodash.truncate@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" - integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== - lodash@4.17.21, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -8187,7 +8200,14 @@ minimatch@3.0.5: dependencies: brace-expansion "^1.1.7" -minimatch@^3.0.4: +minimatch@9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity "sha1-Gc0ZS/0+Qo8EmnCBfAONiatL41s= sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==" @@ -8895,7 +8915,7 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" -optionator@^0.9.1: +optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== @@ -9343,11 +9363,6 @@ process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -progress@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - promise-all-reject-late@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/promise-all-reject-late/-/promise-all-reject-late-1.0.1.tgz#f8ebf13483e5ca91ad809ccc2fcf25f26f8643c2" @@ -9630,11 +9645,6 @@ regenerator-runtime@^0.14.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== -regexpp@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== - remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -9650,11 +9660,6 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= -require-from-string@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== - resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -9916,15 +9921,6 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - smart-buffer@^4.1.0, smart-buffer@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" @@ -10255,7 +10251,7 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -10323,17 +10319,6 @@ synchronous-promise@^2.0.13: resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.15.tgz#07ca1822b9de0001f5ff73595f3d08c4f720eb8e" integrity sha512-k8uzYIkIVwmT+TcglpdN50pS2y1BDcUnBPK9iJeGu0Pl1lOI8pD6wtzgw91Pjpe+RxtTncw32tLxs/R0yNL2Mg== -table@^6.0.9: - version "6.8.1" - resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" - integrity sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA== - dependencies: - ajv "^8.0.1" - lodash.truncate "^4.4.2" - slice-ansi "^4.0.0" - string-width "^4.2.3" - strip-ansi "^6.0.1" - taketalk@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/taketalk/-/taketalk-1.0.0.tgz#b4d4f0deed206ae7df775b129ea2ca6de52f26dd" @@ -10807,11 +10792,6 @@ v8-compile-cache@2.3.0: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== -v8-compile-cache@^2.0.3: - version "2.4.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz#cdada8bec61e15865f05d097c5f4fd30e94dc128" - integrity sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw== - v8-to-istanbul@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz#b6f994b0b5d4ef255e17a0d17dc444a9f5132fa4" From 6da4748cc28bb727c0ca5cde46f7ec5093241311 Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Wed, 17 Jan 2024 14:23:03 -0500 Subject: [PATCH 05/38] Fix typescript eslint in perf tests (#4550) --- ironfish/src/workerPool/tasks/workerMessages.test.perf.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ironfish/src/workerPool/tasks/workerMessages.test.perf.ts b/ironfish/src/workerPool/tasks/workerMessages.test.perf.ts index 1b2a919798..f828a46dc4 100644 --- a/ironfish/src/workerPool/tasks/workerMessages.test.perf.ts +++ b/ironfish/src/workerPool/tasks/workerMessages.test.perf.ts @@ -13,7 +13,7 @@ import { writeTestReport, } from '../../testUtilities' import { BenchUtils, CurrencyUtils, PromiseUtils, SegmentResults } from '../../utils' -import { Account } from '../../wallet' +import { SpendingAccount } from '../../wallet' import { CreateMinersFeeRequest } from './createMinersFee' import { DecryptNoteOptions, DecryptNotesRequest } from './decryptNotes' import { WORKER_MESSAGE_HEADER_SIZE } from './workerMessage' @@ -23,7 +23,7 @@ describe('WorkerMessages', () => { const TEST_ITERATIONS = 50 - let account: Account + let account: SpendingAccount beforeAll(async () => { account = await useAccountFixture(nodeTest.wallet, 'account') From d3099756c20e4742393f6560f6695aed12e6dfda Mon Sep 17 00:00:00 2001 From: jowparks Date: Wed, 17 Jan 2024 13:20:05 -0800 Subject: [PATCH 06/38] adds build method in napi bindings for ProposedTransaction.build() (#4548) --- .../src/structs/transaction.rs | 42 ++++++++++++++++++- ironfish-rust/src/lib.rs | 3 ++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/ironfish-rust-nodejs/src/structs/transaction.rs b/ironfish-rust-nodejs/src/structs/transaction.rs index 57417b7dec..010dbbd42c 100644 --- a/ironfish-rust-nodejs/src/structs/transaction.rs +++ b/ironfish-rust-nodejs/src/structs/transaction.rs @@ -6,11 +6,15 @@ use std::cell::RefCell; use std::convert::TryInto; use ironfish::assets::asset_identifier::AssetIdentifier; +use ironfish::serializing::hex_to_bytes; use ironfish::transaction::{ batch_verify_transactions, TransactionVersion, TRANSACTION_EXPIRATION_SIZE, TRANSACTION_FEE_SIZE, TRANSACTION_PUBLIC_KEY_SIZE, TRANSACTION_SIGNATURE_SIZE, }; -use ironfish::{MerkleNoteHash, ProposedTransaction, PublicAddress, SaplingKey, Transaction}; +use ironfish::{ + MerkleNoteHash, OutgoingViewKey, ProofGenerationKey, ProofGenerationKeySerializable, + ProposedTransaction, PublicAddress, SaplingKey, Transaction, ViewKey, +}; use napi::{ bindgen_prelude::{i64n, BigInt, Buffer, Env, Object, Result, Undefined}, JsBuffer, @@ -308,6 +312,42 @@ impl NativeTransaction { Ok(Buffer::from(vec)) } + // Outputs buffer of an unsigned transaction + #[napi] + pub fn build( + &mut self, + proof_generation_key_str: String, + view_key_str: String, + outgoing_view_key_str: String, + public_address_str: String, + intended_transaction_fee: BigInt, + ) -> Result { + let view_key = ViewKey::from_hex(&view_key_str).map_err(to_napi_err)?; + let outgoing_view_key = + OutgoingViewKey::from_hex(&outgoing_view_key_str).map_err(to_napi_err)?; + let public_address = PublicAddress::from_hex(&public_address_str).map_err(to_napi_err)?; + let proof_generation_key = ProofGenerationKey::deserialize( + hex_to_bytes(&proof_generation_key_str) + .map_err(|_| to_napi_err("PublicKeyPackage hex to bytes failed"))?, + ) + .map_err(to_napi_err)?; + let unsigned_transaction = self + .transaction + .build( + proof_generation_key, + view_key, + outgoing_view_key, + public_address, + intended_transaction_fee.get_i64().0, + ) + .map_err(to_napi_err)?; + + let mut vec: Vec = vec![]; + unsigned_transaction.write(&mut vec).map_err(to_napi_err)?; + + Ok(Buffer::from(vec)) + } + #[napi] pub fn set_expiration(&mut self, sequence: u32) -> Undefined { self.transaction.set_expiration(sequence); diff --git a/ironfish-rust/src/lib.rs b/ironfish-rust/src/lib.rs index 477004022c..ac99da0682 100644 --- a/ironfish-rust/src/lib.rs +++ b/ironfish-rust/src/lib.rs @@ -19,7 +19,10 @@ pub mod signal_catcher; pub mod transaction; pub mod util; pub mod witness; +pub use ironfish_zkp::primitives::ProofGenerationKeySerializable; +pub use ironfish_zkp::ProofGenerationKey; pub use { + ironfish_frost::frost, keys::{IncomingViewKey, OutgoingViewKey, PublicAddress, SaplingKey, ViewKey}, merkle_note::MerkleNote, merkle_note_hash::MerkleNoteHash, From 66770833320b004f3d2c21f7d5d8185a74137e7f Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Wed, 17 Jan 2024 13:38:22 -0800 Subject: [PATCH 07/38] setting route test isready and synced to true (#4552) --- ironfish/src/rpc/routes/wallet/sendTransaction.test.slow.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ironfish/src/rpc/routes/wallet/sendTransaction.test.slow.ts b/ironfish/src/rpc/routes/wallet/sendTransaction.test.slow.ts index 41a08e230e..b04a0277a8 100644 --- a/ironfish/src/rpc/routes/wallet/sendTransaction.test.slow.ts +++ b/ironfish/src/rpc/routes/wallet/sendTransaction.test.slow.ts @@ -9,11 +9,14 @@ import { createRouteTest } from '../../../testUtilities/routeTest' import { AsyncUtils } from '../../../utils' describe('Route wallet/sendTransaction (with note selection)', () => { - const routeTest = createRouteTest(true) + const routeTest = createRouteTest() it('spends the specified notes', async () => { const sender = await useAccountFixture(routeTest.node.wallet, 'accountA') + routeTest.node.peerNetwork['_isReady'] = true + routeTest.chain.synced = true + const block = await useMinerBlockFixture( routeTest.chain, undefined, From 0ed026dca6c460d3f8fd512370c9e61692924dcb Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Wed, 17 Jan 2024 14:44:55 -0800 Subject: [PATCH 08/38] adds optional multiSigKeys to Account type (#4547) * adds optional multiSigKeys to Account type includes multiSigKeys (identifier, keyPackage, proofGenerationKey) in Account constructor throws an error if an account is created that includes both multiSigKeys and a non-null spendingKey * removes assertion from constructor we should validate input in the import logic instead of in the constructor --- ironfish/src/wallet/account/account.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ironfish/src/wallet/account/account.ts b/ironfish/src/wallet/account/account.ts index 04aa984b6a..c009c748c5 100644 --- a/ironfish/src/wallet/account/account.ts +++ b/ironfish/src/wallet/account/account.ts @@ -47,6 +47,11 @@ export class Account { createdAt: HeadValue | null readonly prefix: Buffer readonly prefixRange: DatabaseKeyRange + readonly multiSigKeys?: { + identifier: string + keyPackage: string + proofGenerationKey: string + } constructor({ id, @@ -59,6 +64,7 @@ export class Account { outgoingViewKey, version, createdAt, + multiSigKeys, }: AccountValue & { walletDb: WalletDB }) { this.id = id this.name = name @@ -76,6 +82,7 @@ export class Account { this.walletDb = walletDb this.version = version ?? 1 this.createdAt = createdAt + this.multiSigKeys = multiSigKeys } isSpendingAccount(): this is SpendingAccount { From 06a9f03225a303d98c6d74513be92411968a9f37 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Wed, 17 Jan 2024 15:04:14 -0800 Subject: [PATCH 09/38] Rahul/ifl 2044 1 add rust method to split a sapling key into shares for FROST (#4551) * split secret implementation * adding tests * test for split secret * running formatter * changing keyparts type to Vec<_> * removing comment * moving test to tests.rc --- ironfish-rust/src/transaction/mod.rs | 55 +++++++++++++++++++++++++- ironfish-rust/src/transaction/tests.rs | 36 ++++++++++++++++- 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/ironfish-rust/src/transaction/mod.rs b/ironfish-rust/src/transaction/mod.rs index eef00bbff3..cdfc04380c 100644 --- a/ironfish-rust/src/transaction/mod.rs +++ b/ironfish-rust/src/transaction/mod.rs @@ -21,12 +21,23 @@ use crate::{ OutgoingViewKey, OutputDescription, SpendDescription, ViewKey, }; +use ironfish_frost::frost; +use ironfish_frost::frost::Error; +use rand::{ + rngs::{OsRng, ThreadRng}, + thread_rng, +}; + +use ironfish_frost::frost::{ + keys::{IdentifierList, KeyPackage, PublicKeyPackage}, + Identifier, SigningKey, +}; + use bellperson::groth16::{verify_proofs_batch, PreparedVerifyingKey}; use blake2b_simd::Params as Blake2b; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use group::GroupEncoding; use jubjub::ExtendedPoint; -use rand::{rngs::OsRng, thread_rng}; use ironfish_zkp::{ constants::{ @@ -38,6 +49,7 @@ use ironfish_zkp::{ }; use std::{ + collections::HashMap, io::{self, Write}, iter, slice::Iter, @@ -935,3 +947,44 @@ pub fn batch_verify_transactions<'a>( &SAPLING.mint_verifying_key, ) } + +pub struct SecretShareConfig { + pub min_signers: u16, + pub max_signers: u16, + pub secret: Vec, +} + +pub fn split_secret( + config: &SecretShareConfig, + identifiers: IdentifierList, + rng: &mut ThreadRng, +) -> Result<(HashMap, PublicKeyPackage), Error> { + let secret_key = SigningKey::deserialize( + config + .secret + .clone() + .try_into() + .map_err(|_| Error::MalformedSigningKey)?, + )?; + + let (shares, pubkeys) = frost::keys::split( + &secret_key, + config.max_signers, + config.min_signers, + identifiers, + rng, + )?; + + for (_k, v) in shares.clone() { + frost::keys::KeyPackage::try_from(v)?; + } + + let mut key_packages: HashMap<_, _> = HashMap::new(); + + for (identifier, secret_share) in shares { + let key_package = frost::keys::KeyPackage::try_from(secret_share.clone()).unwrap(); + key_packages.insert(identifier, key_package); + } + + Ok((key_packages, pubkeys)) +} diff --git a/ironfish-rust/src/transaction/tests.rs b/ironfish-rust/src/transaction/tests.rs index a8f79fa9da..6c65a3601e 100644 --- a/ironfish-rust/src/transaction/tests.rs +++ b/ironfish-rust/src/transaction/tests.rs @@ -14,12 +14,14 @@ use crate::{ sapling_bls12::SAPLING, test_util::make_fake_witness, transaction::{ - batch_verify_transactions, verify_transaction, TransactionVersion, - TRANSACTION_EXPIRATION_SIZE, TRANSACTION_FEE_SIZE, TRANSACTION_SIGNATURE_SIZE, + batch_verify_transactions, split_secret, verify_transaction, SecretShareConfig, + TransactionVersion, TRANSACTION_EXPIRATION_SIZE, TRANSACTION_FEE_SIZE, + TRANSACTION_SIGNATURE_SIZE, }, }; use ff::Field; +use ironfish_frost::frost::{frost::keys::reconstruct, JubjubBlake2b512}; use ironfish_zkp::{ constants::{ASSET_ID_LENGTH, SPENDING_KEY_GENERATOR, TREE_DEPTH}, proofs::{MintAsset, Output, Spend}, @@ -643,3 +645,33 @@ fn test_batch_verify() { Err(e) if matches!(e.kind, IronfishErrorKind::InvalidSpendSignature) )); } + +#[test] +fn test_split_secret() { + let mut rng = rand::thread_rng(); + + let key = SaplingKey::generate_key().spend_authorizing_key.to_bytes(); + + let config = SecretShareConfig { + min_signers: 2, + max_signers: 3, + secret: key.to_vec(), + }; + + let (key_packages, _) = split_secret( + &config, + ironfish_frost::frost::keys::IdentifierList::Default, + &mut rng, + ) + .unwrap(); + assert_eq!(key_packages.len(), 3); + + let key_parts: Vec<_> = key_packages.values().cloned().collect(); + + let signing_key = + reconstruct::(&key_parts).expect("key reconstruction failed"); + + let scalar = signing_key.to_scalar(); + + assert_eq!(scalar.to_bytes(), key); +} From fc8855dbaef459e6cb2a1c4d11db3ad78701094a Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Wed, 17 Jan 2024 15:18:53 -0800 Subject: [PATCH 10/38] stores multiSigKeys as hex instead of utf8 (#4553) uses hex strings and stores multisig keys as buffers in the wallet db instead of using utf8 strings this more closely matches the precedent set with our other keys and may simplify ser/de between TypeScript and Rust --- .../src/wallet/walletdb/accountValue.test.ts | 6 +++--- ironfish/src/wallet/walletdb/accountValue.ts | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ironfish/src/wallet/walletdb/accountValue.test.ts b/ironfish/src/wallet/walletdb/accountValue.test.ts index fda4eab513..686b6d7f2c 100644 --- a/ironfish/src/wallet/walletdb/accountValue.test.ts +++ b/ironfish/src/wallet/walletdb/accountValue.test.ts @@ -45,9 +45,9 @@ describe('AccountValueEncoding', () => { version: 1, createdAt: null, multiSigKeys: { - identifier: 'a', - keyPackage: 'b', - proofGenerationKey: 'c', + identifier: 'deaf', + keyPackage: 'beef', + proofGenerationKey: 'feed', }, } const buffer = encoder.serialize(value) diff --git a/ironfish/src/wallet/walletdb/accountValue.ts b/ironfish/src/wallet/walletdb/accountValue.ts index fbafdc32fb..95907331b6 100644 --- a/ironfish/src/wallet/walletdb/accountValue.ts +++ b/ironfish/src/wallet/walletdb/accountValue.ts @@ -55,9 +55,9 @@ export class AccountValueEncoding implements IDatabaseEncoding { } if (value.multiSigKeys) { - bw.writeVarString(value.multiSigKeys.identifier, 'utf8') - bw.writeVarString(value.multiSigKeys.keyPackage, 'utf8') - bw.writeVarString(value.multiSigKeys.proofGenerationKey, 'utf8') + bw.writeVarBytes(Buffer.from(value.multiSigKeys.identifier, 'hex')) + bw.writeVarBytes(Buffer.from(value.multiSigKeys.keyPackage, 'hex')) + bw.writeVarBytes(Buffer.from(value.multiSigKeys.proofGenerationKey, 'hex')) } return bw.render() @@ -87,9 +87,9 @@ export class AccountValueEncoding implements IDatabaseEncoding { let multiSigKeys = undefined if (hasMultiSigKeys) { multiSigKeys = { - identifier: reader.readVarString('utf8'), - keyPackage: reader.readVarString('utf8'), - proofGenerationKey: reader.readVarString('utf8'), + identifier: reader.readVarBytes().toString('hex'), + keyPackage: reader.readVarBytes().toString('hex'), + proofGenerationKey: reader.readVarBytes().toString('hex'), } } @@ -125,9 +125,9 @@ export class AccountValueEncoding implements IDatabaseEncoding { size += encoding.nonNullSize } if (value.multiSigKeys) { - size += bufio.sizeVarString(value.multiSigKeys.identifier, 'utf8') - size += bufio.sizeVarString(value.multiSigKeys.keyPackage, 'utf8') - size += bufio.sizeVarString(value.multiSigKeys.proofGenerationKey, 'utf8') + size += bufio.sizeVarString(value.multiSigKeys.identifier, 'hex') + size += bufio.sizeVarString(value.multiSigKeys.keyPackage, 'hex') + size += bufio.sizeVarString(value.multiSigKeys.proofGenerationKey, 'hex') } return size From 76819fb423ee734e7931af3c90dfa939a960b555 Mon Sep 17 00:00:00 2001 From: jowparks Date: Wed, 17 Jan 2024 17:22:26 -0800 Subject: [PATCH 11/38] ironfish round one wrapper for core frost (#4554) --- .gitignore | 1 + ironfish-rust/src/frost_utils/mod.rs | 5 ++ ironfish-rust/src/frost_utils/round_one.rs | 56 ++++++++++++++++++++++ ironfish-rust/src/lib.rs | 1 + 4 files changed, 63 insertions(+) create mode 100644 ironfish-rust/src/frost_utils/mod.rs create mode 100644 ironfish-rust/src/frost_utils/round_one.rs diff --git a/.gitignore b/.gitignore index c8126cc6c5..0a5751c5b2 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ testdbs .env.production.local */**/yarn.lock .idea +.vscode # logs npm-debug.log* diff --git a/ironfish-rust/src/frost_utils/mod.rs b/ironfish-rust/src/frost_utils/mod.rs new file mode 100644 index 0000000000..25bb98d3e2 --- /dev/null +++ b/ironfish-rust/src/frost_utils/mod.rs @@ -0,0 +1,5 @@ +/* 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/. */ + +pub mod round_one; diff --git a/ironfish-rust/src/frost_utils/round_one.rs b/ironfish-rust/src/frost_utils/round_one.rs new file mode 100644 index 0000000000..3b6c4177d1 --- /dev/null +++ b/ironfish-rust/src/frost_utils/round_one.rs @@ -0,0 +1,56 @@ +/* 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_frost::frost::{ + self, + keys::KeyPackage, + round1::{SigningCommitments, SigningNonces}, +}; +use rand::{rngs::StdRng, SeedableRng}; + +// Small wrapper around frost::round1::commit that provides a seedable rng +pub fn round_one(key_package: &KeyPackage, seed: u64) -> (SigningNonces, SigningCommitments) { + let mut rng = StdRng::seed_from_u64(seed); + frost::round1::commit(key_package.signing_share(), &mut rng) +} + +#[cfg(test)] +mod test { + + use ff::Field; + use ironfish_frost::frost::keys::IdentifierList; + use jubjub::Fr; + use rand::rngs::ThreadRng; + + use crate::transaction::{split_secret, SecretShareConfig}; + + #[test] + pub fn test_seed_provides_same_result() { + let seed = 100; + let key = Fr::random(&mut rand::thread_rng()); + + let mut rng = ThreadRng::default(); + let key_packages = split_secret( + &SecretShareConfig { + max_signers: 3, + min_signers: 2, + secret: key.to_bytes().to_vec(), + }, + IdentifierList::Default, + &mut rng, + ) + .expect("key shares to be created"); + let key_package = key_packages + .0 + .into_iter() + .next() + .expect("key package to be created") + .1; + let (nonces, commitments) = super::round_one(&key_package, seed); + let (nonces2, commitments2) = super::round_one(&key_package, seed); + assert_eq!(nonces.hiding().serialize(), nonces2.hiding().serialize()); + assert_eq!(nonces.binding().serialize(), nonces2.binding().serialize()); + assert_eq!(commitments, commitments2); + } +} diff --git a/ironfish-rust/src/lib.rs b/ironfish-rust/src/lib.rs index ac99da0682..f20dfeb45b 100644 --- a/ironfish-rust/src/lib.rs +++ b/ironfish-rust/src/lib.rs @@ -6,6 +6,7 @@ use blstrs::Bls12; pub mod assets; pub mod errors; +pub mod frost_utils; pub mod keys; pub mod merkle_note; pub mod merkle_note_hash; From 96bf135722bde7cd7b995793b928669a40a85d65 Mon Sep 17 00:00:00 2001 From: Daniel Cogan Date: Thu, 18 Jan 2024 11:14:20 -0700 Subject: [PATCH 12/38] Add FishHash to node verification (#4536) --- ironfish/src/blockHasher.test.ts | 165 ++++++++++++++++++++++ ironfish/src/blockHasher.ts | 73 ++++++++++ ironfish/src/consensus/consensus.test.ts | 3 + ironfish/src/consensus/consensus.ts | 7 + ironfish/src/defaultNetworkDefinitions.ts | 9 +- ironfish/src/networkDefinition.ts | 1 + ironfish/src/node.ts | 13 +- ironfish/src/sdk.ts | 3 +- ironfish/src/strategy.test.slow.ts | 141 ++++++++++++++---- ironfish/src/strategy.test.ts | 8 +- ironfish/src/strategy.ts | 35 ++--- ironfish/src/testUtilities/nodeTest.ts | 9 +- 12 files changed, 409 insertions(+), 58 deletions(-) create mode 100644 ironfish/src/blockHasher.test.ts create mode 100644 ironfish/src/blockHasher.ts diff --git a/ironfish/src/blockHasher.test.ts b/ironfish/src/blockHasher.test.ts new file mode 100644 index 0000000000..523ef6e06a --- /dev/null +++ b/ironfish/src/blockHasher.test.ts @@ -0,0 +1,165 @@ +/* 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 { blake3 } from '@napi-rs/blake-hash' +import { BlockHasher, serializeHeaderBlake3, serializeHeaderFishHash } from './blockHasher' +import { TestnetConsensus } from './consensus' +import { Target } from './primitives' +import { RawBlockHeader } from './primitives/blockheader' +import { FISH_HASH_CONTEXT } from './testUtilities' + +const consensusParameters = { + allowedBlockFutureSeconds: 15, + genesisSupplyInIron: 42000000, + targetBlockTimeInSeconds: 60, + targetBucketTimeInSeconds: 10, + maxBlockSizeBytes: 524288, + minFee: 0, + enableAssetOwnership: 1, + enforceSequentialBlockTime: 1, + enableFishHash: 100001, +} + +describe('Hashes blocks with correct hashing algorithm', () => { + let blockHasher: BlockHasher + + beforeAll(() => { + const modifiedConsensus = new TestnetConsensus(consensusParameters) + blockHasher = new BlockHasher({ + consensus: modifiedConsensus, + context: FISH_HASH_CONTEXT, + }) + }) + + const rawHeaderFields = { + previousBlockHash: Buffer.alloc(32, 'previous'), + noteCommitment: Buffer.alloc(32, 'header'), + transactionCommitment: Buffer.alloc(32, 'transactionRoot'), + target: new Target(17), + randomness: BigInt(25), + timestamp: new Date(1598467858637), + graffiti: Buffer.alloc(32, 'graffiti'), + } + + it('Hashes block headers with blake3 before the activation sequence', () => { + const rawHeader = { + ...rawHeaderFields, + sequence: consensusParameters.enableFishHash - 1, + } + const hash = blockHasher.hashHeader(rawHeader) + + expect(hash.equals(blake3(serializeHeaderBlake3(rawHeader)))).toBe(true) + + expect(hash.equals(blake3(serializeHeaderFishHash(rawHeader)))).toBe(false) + expect(hash.equals(FISH_HASH_CONTEXT.hash(serializeHeaderBlake3(rawHeader)))).toBe(false) + expect(hash.equals(FISH_HASH_CONTEXT.hash(serializeHeaderFishHash(rawHeader)))).toBe(false) + }) + + it('Hashes block headers with FishHash after the activation sequence', () => { + const rawHeader = { + ...rawHeaderFields, + sequence: consensusParameters.enableFishHash, + } + const hash = blockHasher.hashHeader(rawHeader) + + expect(hash.equals(FISH_HASH_CONTEXT.hash(serializeHeaderFishHash(rawHeader)))).toBe(true) + + expect(hash.equals(FISH_HASH_CONTEXT.hash(serializeHeaderBlake3(rawHeader)))).toBe(false) + expect(hash.equals(blake3(serializeHeaderFishHash(rawHeader)))).toBe(false) + expect(hash.equals(blake3(serializeHeaderBlake3(rawHeader)))).toBe(false) + }) + + it('Puts graffiti in front of serialized block header for FishHash', () => { + const rawHeader = { + ...rawHeaderFields, + sequence: consensusParameters.enableFishHash, + } + + const serialized = serializeHeaderFishHash(rawHeader) + expect(serialized.toString('hex', 0, 32)).toEqual(rawHeader.graffiti.toString('hex')) + }) + + it('Hashes existing mainnet blocks correctly', () => { + const block500: RawBlockHeader = { + sequence: 500, + previousBlockHash: Buffer.from( + '000000000000005a307332b6910b730347c1849e4f7772d0c30cf251f7a231b8', + 'hex', + ), + timestamp: new Date(1682046624440), + graffiti: Buffer.from( + '6865726f6d696e6572732e636f6d20575053474a35796a50500702023eb36ed2', + 'hex', + ), + noteCommitment: Buffer.from( + '365b8d520119591d4adda9a43586732151759bcbab2663dc810df6b479a08557', + 'hex', + ), + transactionCommitment: Buffer.from( + 'fd54b5407c50c45114bc36bc18c6093015f3edd614fff12c4871f02011a2a3cc', + 'hex', + ), + target: new Target(756384800508438608204615908259480917815960905158618695086860n), + randomness: 2734524376649158465n, + } + + const block10000: RawBlockHeader = { + sequence: 10000, + previousBlockHash: Buffer.from( + '000000000000001918ec5e350ec40d3606c865c162439bf1faebf9c23c4689ad', + 'hex', + ), + timestamp: new Date(1682604438528), + graffiti: Buffer.from( + '0000000000000000000000000000000000000000000000008308423f00000000', + 'hex', + ), + noteCommitment: Buffer.from( + 'a6ada17d6dfa066198030403d4b6160633a7064843bd7c7124d9669069c9666b', + 'hex', + ), + transactionCommitment: Buffer.from( + 'a8f937d95020a51c7f7ecf7157d330ee146aeb6cbd4fcce4a42e80db4f6a1f4e', + 'hex', + ), + target: new Target(429471422073515230604015821651699605609508434072272216389555n), + randomness: 2459773558139518993n, + } + + const block100000: RawBlockHeader = { + sequence: 100000, + previousBlockHash: Buffer.from( + '00000000000000726356321bf7c2057feaeaeba771cf997ac6f13038b7fdf9ab', + 'hex', + ), + timestamp: new Date(1687943221494), + graffiti: Buffer.from( + '00000000000000000000000000000000a0b0d100000000000000000023000000', + 'hex', + ), + noteCommitment: Buffer.from( + '9094c2534f0ee3ae7ed078a3811e2b6de47094f9d710e2ae26ed00e038653819', + 'hex', + ), + transactionCommitment: Buffer.from( + '97c767928661a813038a44306fd3a5b050ed685c5584d739e0c1833c0123f792', + 'hex', + ), + target: new Target(946788478496387895228612647492461330784782254609709025541178n), + randomness: 50540119513756614n, + } + + expect(blockHasher.hashHeader(block500).toString('hex')).toEqual( + '0000000000000032a29b02858a9d22312e1bf7d15bc6c8e36215b0c79b76ab5b', + ) + + expect(blockHasher.hashHeader(block10000).toString('hex')).toEqual( + '0000000000000038cf8ac6f148f2c5edae32b5238c9f0ef53e6fe0aa4cc68043', + ) + + expect(blockHasher.hashHeader(block100000).toString('hex')).toEqual( + '000000000000003eb2e4dfacbcc93079415a784bd7e0887f3375679b2dea7713', + ) + }) +}) diff --git a/ironfish/src/blockHasher.ts b/ironfish/src/blockHasher.ts new file mode 100644 index 0000000000..89ac8daa94 --- /dev/null +++ b/ironfish/src/blockHasher.ts @@ -0,0 +1,73 @@ +/* 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 { FishHashContext } from '@ironfish/rust-nodejs' +import { blake3 } from '@napi-rs/blake-hash' +import bufio from 'bufio' +import { Assert } from './assert' +import { Consensus } from './consensus' +import { BlockHash, RawBlockHeader } from './primitives/blockheader' + +export class BlockHasher { + private readonly consensus: Consensus + private readonly fishHashContext: FishHashContext | null = null + + constructor(options: { + consensus: Consensus + fullContext?: boolean + context?: FishHashContext + }) { + this.consensus = options.consensus + if (this.consensus.parameters.enableFishHash !== 'never') { + this.fishHashContext = options.context + ? options.context + : new FishHashContext(!!options.fullContext) + } + } + + hashHeader(header: RawBlockHeader): BlockHash { + const useFishHash = this.consensus.isActive( + this.consensus.parameters.enableFishHash, + header.sequence, + ) + + if (useFishHash) { + Assert.isNotNull(this.fishHashContext, 'FishHash context was not initialized') + + const serialized = serializeHeaderFishHash(header) + return this.fishHashContext.hash(serialized) + } + + const serialized = serializeHeaderBlake3(header) + return blake3(serialized) + } +} + +export function serializeHeaderBlake3(header: RawBlockHeader): Buffer { + const bw = bufio.write(180) + bw.writeBigU64BE(header.randomness) + bw.writeU32(header.sequence) + bw.writeHash(header.previousBlockHash) + bw.writeHash(header.noteCommitment) + bw.writeHash(header.transactionCommitment) + bw.writeBigU256BE(header.target.asBigInt()) + bw.writeU64(header.timestamp.getTime()) + bw.writeBytes(header.graffiti) + + return bw.render() +} + +export function serializeHeaderFishHash(header: RawBlockHeader): Buffer { + const bw = bufio.write(180) + bw.writeBytes(header.graffiti) + bw.writeU32(header.sequence) + bw.writeHash(header.previousBlockHash) + bw.writeHash(header.noteCommitment) + bw.writeHash(header.transactionCommitment) + bw.writeBigU256BE(header.target.asBigInt()) + bw.writeU64(header.timestamp.getTime()) + bw.writeBigU64BE(header.randomness) + + return bw.render() +} diff --git a/ironfish/src/consensus/consensus.test.ts b/ironfish/src/consensus/consensus.test.ts index c67d5861f8..46fcc956f5 100644 --- a/ironfish/src/consensus/consensus.test.ts +++ b/ironfish/src/consensus/consensus.test.ts @@ -15,6 +15,7 @@ describe('Consensus', () => { minFee: 6, enableAssetOwnership: 7, enforceSequentialBlockTime: 1, + enableFishHash: 'never', } let consensus: Consensus @@ -67,9 +68,11 @@ describe('Consensus', () => { minFee: 6, enableAssetOwnership: 'never', enforceSequentialBlockTime: 'never', + enableFishHash: 'never', }) expect(consensus.getActiveTransactionVersion(5)).toEqual(TransactionVersion.V1) expect(consensus.isActive(consensus.parameters.enableAssetOwnership, 3)).toBe(false) expect(consensus.isActive(consensus.parameters.enforceSequentialBlockTime, 3)).toBe(false) + expect(consensus.isActive(consensus.parameters.enableFishHash, 3)).toBe(false) }) }) diff --git a/ironfish/src/consensus/consensus.ts b/ironfish/src/consensus/consensus.ts index 28a8e6dbbd..8c5340d8ae 100644 --- a/ironfish/src/consensus/consensus.ts +++ b/ironfish/src/consensus/consensus.ts @@ -48,6 +48,13 @@ export type ConsensusParameters = { * block we enforce the block timestamps in the sequential order as the block sequences. */ enforceSequentialBlockTime: ActivationSequence + + /** + * Sequence at which to start mining and validating blocks with the FishHash algorithm + * instead Blake3. This sequence also modifies the block header serialization to move graffiti + * to the beginning of the block header before mining. + */ + enableFishHash: ActivationSequence } export class Consensus { diff --git a/ironfish/src/defaultNetworkDefinitions.ts b/ironfish/src/defaultNetworkDefinitions.ts index feefcb6aba..a7e7c76ca9 100644 --- a/ironfish/src/defaultNetworkDefinitions.ts +++ b/ironfish/src/defaultNetworkDefinitions.ts @@ -37,7 +37,8 @@ export const TESTNET = `{ "maxBlockSizeBytes": 524288, "minFee": 1, "enableAssetOwnership": 9999999, - "enforceSequentialBlockTime": "never" + "enforceSequentialBlockTime": "never", + "enableFishHash": "never" } }` @@ -58,7 +59,8 @@ export const MAINNET = ` "maxBlockSizeBytes": 524288, "minFee": 1, "enableAssetOwnership": 9999999, - "enforceSequentialBlockTime": "never" + "enforceSequentialBlockTime": "never", + "enableFishHash": "never" } }` @@ -76,6 +78,7 @@ export const DEVNET = ` "maxBlockSizeBytes": 524288, "minFee": 0, "enableAssetOwnership": 1, - "enforceSequentialBlockTime": 1 + "enforceSequentialBlockTime": 1, + "enableFishHash": "never" } }` diff --git a/ironfish/src/networkDefinition.ts b/ironfish/src/networkDefinition.ts index e3011a4329..238f37864a 100644 --- a/ironfish/src/networkDefinition.ts +++ b/ironfish/src/networkDefinition.ts @@ -49,6 +49,7 @@ export const networkDefinitionSchema: yup.ObjectSchema = yup minFee: yup.number().integer().defined(), enableAssetOwnership: yup.mixed().defined(), enforceSequentialBlockTime: yup.mixed().defined(), + enableFishHash: yup.mixed().defined(), }) .defined(), }) diff --git a/ironfish/src/node.ts b/ironfish/src/node.ts index 828d9a840e..551b282116 100644 --- a/ironfish/src/node.ts +++ b/ironfish/src/node.ts @@ -1,10 +1,11 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { BoxKeyPair } from '@ironfish/rust-nodejs' +import { BoxKeyPair, FishHashContext } from '@ironfish/rust-nodejs' import { v4 as uuid } from 'uuid' import { AssetsVerifier } from './assets' import { Blockchain } from './blockchain' +import { BlockHasher } from './blockHasher' import { TestnetConsensus } from './consensus' import { Config, @@ -196,6 +197,7 @@ export class FullNode { strategyClass, webSocket, privateIdentity, + fishHashContext, }: { pkg: Package dataDir?: string @@ -208,6 +210,7 @@ export class FullNode { strategyClass: typeof Strategy | null webSocket: IsomorphicWebSocketConstructor privateIdentity?: PrivateIdentity + fishHashContext?: FishHashContext }): Promise { logger = logger.withTag('ironfishnode') dataDir = dataDir || DEFAULT_DATA_DIR @@ -247,9 +250,15 @@ export class FullNode { } const consensus = new TestnetConsensus(networkDefinition.consensus) + // TODO: config value for fullContext + const blockHasher = new BlockHasher({ + consensus, + context: fishHashContext, + fullContext: false, + }) strategyClass = strategyClass || Strategy - const strategy = new strategyClass({ workerPool, consensus }) + const strategy = new strategyClass({ workerPool, consensus, blockHasher }) const chain = new Blockchain({ location: config.chainDatabasePath, diff --git a/ironfish/src/sdk.ts b/ironfish/src/sdk.ts index 28fc12b589..27a79144d3 100644 --- a/ironfish/src/sdk.ts +++ b/ironfish/src/sdk.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { BoxKeyPair } from '@ironfish/rust-nodejs' +import { BoxKeyPair, FishHashContext } from '@ironfish/rust-nodejs' import { Config, ConfigOptions, @@ -183,6 +183,7 @@ export class IronfishSdk { }: { autoSeed?: boolean privateIdentity?: PrivateIdentity + fishHashContext?: FishHashContext } = {}): Promise { const webSocket = WebSocketClient as IsomorphicWebSocketConstructor diff --git a/ironfish/src/strategy.test.slow.ts b/ironfish/src/strategy.test.slow.ts index df24bb9627..191bcf5034 100644 --- a/ironfish/src/strategy.test.slow.ts +++ b/ironfish/src/strategy.test.slow.ts @@ -11,18 +11,20 @@ import { Transaction as NativeTransaction, TransactionPosted as NativeTransactionPosted, } from '@ironfish/rust-nodejs' -import { Assert } from './assert' +import { blake3 } from '@napi-rs/blake-hash' +import { BlockHasher, serializeHeaderBlake3, serializeHeaderFishHash } from './blockHasher' import { ConsensusParameters, TestnetConsensus } from './consensus' import { MerkleTree } from './merkletree' import { LeafEncoding } from './merkletree/database/leaves' import { NodeEncoding } from './merkletree/database/nodes' import { NoteHasher } from './merkletree/hasher' -import { Transaction } from './primitives' +import { Target, Transaction } from './primitives' import { Note } from './primitives/note' import { NoteEncrypted, NoteEncryptedHash } from './primitives/noteEncrypted' import { TransactionVersion } from './primitives/transaction' import { BUFFER_ENCODING, IDatabase } from './storage' import { Strategy } from './strategy' +import { FISH_HASH_CONTEXT } from './testUtilities' import { makeDb, makeDbName } from './testUtilities/helpers/storage' import { WorkerPool } from './workerPool' @@ -73,6 +75,7 @@ const consensusParameters: ConsensusParameters = { minFee: 1, enableAssetOwnership: 1, enforceSequentialBlockTime: 3, + enableFishHash: 'never', } /** @@ -91,6 +94,7 @@ describe('Demonstrate the Sapling API', () => { let transaction: NativeTransaction let publicTransaction: NativeTransactionPosted let workerPool: WorkerPool + let strategy: Strategy beforeAll(async () => { // Pay the cost of setting up Sapling and the DB outside of any test @@ -98,6 +102,16 @@ describe('Demonstrate the Sapling API', () => { spenderKey = generateKey() receiverKey = generateKey() workerPool = new WorkerPool() + const consensus = new TestnetConsensus(consensusParameters) + const blockHasher = new BlockHasher({ + consensus, + context: FISH_HASH_CONTEXT, + }) + strategy = new Strategy({ + workerPool, + consensus, + blockHasher, + }) }) describe('Can transact between two accounts', () => { @@ -189,10 +203,6 @@ describe('Demonstrate the Sapling API', () => { describe('Serializes and deserializes transactions', () => { it('Does not hold a posted transaction if no references are taken', async () => { // Generate a miner's fee transaction - const strategy = new Strategy({ - workerPool, - consensus: new TestnetConsensus(consensusParameters), - }) const minersFee = await strategy.createMinersFee(0n, 0, generateKey().spendingKey) expect(minersFee['transactionPosted']).toBeNull() @@ -202,11 +212,6 @@ describe('Demonstrate the Sapling API', () => { it('Holds a posted transaction if a reference is taken', async () => { // Generate a miner's fee transaction - const strategy = new Strategy({ - workerPool, - consensus: new TestnetConsensus(consensusParameters), - }) - const minersFee = await strategy.createMinersFee(0n, 0, generateKey().spendingKey) await minersFee.withReference(async () => { @@ -226,10 +231,6 @@ describe('Demonstrate the Sapling API', () => { it('Does not hold a note if no references are taken', async () => { // Generate a miner's fee transaction const key = generateKey() - const strategy = new Strategy({ - workerPool, - consensus: new TestnetConsensus(consensusParameters), - }) const minersFee = await strategy.createMinersFee(0n, 0, key.spendingKey) expect(minersFee['transactionPosted']).toBeNull() @@ -255,26 +256,33 @@ describe('Demonstrate the Sapling API', () => { }) it('Creates transactions with the correct version based on the sequence', async () => { - const modifiedParams = consensusParameters - modifiedParams.enableAssetOwnership = 1234 + const modifiedParams = { + ...consensusParameters, + enableAssetOwnership: 1234, + } + const key = generateKey() - const strategy = new Strategy({ + const modifiedConsensus = new TestnetConsensus(modifiedParams) + const blockHasher = new BlockHasher({ + consensus: modifiedConsensus, + context: FISH_HASH_CONTEXT, + }) + const modifiedStrategy = new Strategy({ workerPool, - consensus: new TestnetConsensus(modifiedParams), + consensus: modifiedConsensus, + blockHasher, }) - Assert.isTrue(typeof consensusParameters.enableAssetOwnership === 'number') - const enableAssetOwnershipSequence = Number(consensusParameters.enableAssetOwnership) - const minersFee1 = await strategy.createMinersFee( + const minersFee1 = await modifiedStrategy.createMinersFee( 0n, - enableAssetOwnershipSequence - 1, + modifiedParams.enableAssetOwnership - 1, key.spendingKey, ) expect(minersFee1.version()).toEqual(TransactionVersion.V1) - const minersFee2 = await strategy.createMinersFee( + const minersFee2 = await modifiedStrategy.createMinersFee( 0n, - enableAssetOwnershipSequence, + modifiedParams.enableAssetOwnership, key.spendingKey, ) expect(minersFee2.version()).toEqual(TransactionVersion.V2) @@ -350,4 +358,87 @@ describe('Demonstrate the Sapling API', () => { expect(await workerPool.verifyTransactions([postedTransaction])).toEqual({ valid: true }) }) }) + + describe('Hashes blocks with correct hashing algorithm', () => { + let modifiedStrategy: Strategy + const modifiedParams = { + ...consensusParameters, + enableFishHash: 10, + } + + beforeAll(() => { + const modifiedConsensus = new TestnetConsensus(modifiedParams) + const blockHasher = new BlockHasher({ + consensus: modifiedConsensus, + context: FISH_HASH_CONTEXT, + }) + modifiedStrategy = new Strategy({ + workerPool, + consensus: modifiedConsensus, + blockHasher, + }) + }) + + const rawHeaderFields = { + previousBlockHash: Buffer.alloc(32), + noteCommitment: Buffer.alloc(32, 'header'), + transactionCommitment: Buffer.alloc(32, 'transactionRoot'), + target: new Target(17), + randomness: BigInt(25), + timestamp: new Date(1598467858637), + graffiti: Buffer.alloc(32), + } + + it('Creates block headers with blake3 before the activation sequence', () => { + const rawHeader = { + ...rawHeaderFields, + sequence: modifiedParams.enableFishHash - 1, + } + const header = modifiedStrategy.newBlockHeader(rawHeader) + + expect(header.hash.equals(blake3(serializeHeaderBlake3(rawHeader)))).toBe(true) + + expect(header.hash.equals(blake3(serializeHeaderFishHash(rawHeader)))).not.toBe(true) + expect( + header.hash.equals(FISH_HASH_CONTEXT.hash(serializeHeaderBlake3(rawHeader))), + ).not.toBe(true) + + expect(header.toRaw()).toEqual(rawHeader) + }) + + it('Creates block headers with FishHash after the activation sequence', () => { + const rawHeader = { + ...rawHeaderFields, + sequence: modifiedParams.enableFishHash, + } + const header = modifiedStrategy.newBlockHeader(rawHeader) + + expect( + header.hash.equals(FISH_HASH_CONTEXT.hash(serializeHeaderFishHash(rawHeader))), + ).toBe(true) + + expect( + header.hash.equals(FISH_HASH_CONTEXT.hash(serializeHeaderBlake3(rawHeader))), + ).not.toBe(true) + + expect(header.hash.equals(blake3(serializeHeaderFishHash(rawHeader)))).not.toBe(true) + + expect(header.toRaw()).toEqual(rawHeader) + }) + + it('Creates block headers with noteSize and work if passed in', () => { + const rawHeader = { + ...rawHeaderFields, + sequence: modifiedParams.enableFishHash - 1, + } + + const header1 = modifiedStrategy.newBlockHeader(rawHeader) + expect(header1.noteSize).toBeNull() + expect(header1.work).toEqual(BigInt(0)) + + const header2 = modifiedStrategy.newBlockHeader(rawHeader, 123, BigInt(456)) + expect(header2.noteSize).toEqual(123) + expect(header2.work).toEqual(BigInt(456)) + }) + }) }) diff --git a/ironfish/src/strategy.test.ts b/ironfish/src/strategy.test.ts index c304ce24f6..163613fc84 100644 --- a/ironfish/src/strategy.test.ts +++ b/ironfish/src/strategy.test.ts @@ -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/. */ +import { BlockHasher } from './blockHasher' import { Consensus, ConsensusParameters } from './consensus' import { Strategy } from './strategy' +import { FISH_HASH_CONTEXT } from './testUtilities' import { WorkerPool } from './workerPool' describe('Miners reward', () => { @@ -18,12 +20,16 @@ describe('Miners reward', () => { minFee: 1, enableAssetOwnership: 1, enforceSequentialBlockTime: 3, + enableFishHash: 'never', } beforeAll(() => { + const consensus = new Consensus(consensusParameters) + const blockHasher = new BlockHasher({ consensus, context: FISH_HASH_CONTEXT }) strategy = new Strategy({ workerPool: new WorkerPool(), - consensus: new Consensus(consensusParameters), + consensus, + blockHasher, }) }) diff --git a/ironfish/src/strategy.ts b/ironfish/src/strategy.ts index 95bb2eabca..31825dc22b 100644 --- a/ironfish/src/strategy.ts +++ b/ironfish/src/strategy.ts @@ -1,12 +1,10 @@ /* 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 { blake3 } from '@napi-rs/blake-hash' -import bufio from 'bufio' +import { BlockHasher } from './blockHasher' import { Consensus } from './consensus' import { Block, RawBlock } from './primitives/block' -import { BlockHash, BlockHeader, RawBlockHeader } from './primitives/blockheader' +import { BlockHeader, RawBlockHeader } from './primitives/blockheader' import { Transaction } from './primitives/transaction' import { MathUtils } from './utils' import { WorkerPool } from './workerPool' @@ -17,13 +15,19 @@ import { WorkerPool } from './workerPool' export class Strategy { readonly workerPool: WorkerPool readonly consensus: Consensus + readonly blockHasher: BlockHasher private miningRewardCachedByYear: Map - constructor(options: { workerPool: WorkerPool; consensus: Consensus }) { + constructor(options: { + workerPool: WorkerPool + consensus: Consensus + blockHasher: BlockHasher + }) { this.miningRewardCachedByYear = new Map() this.workerPool = options.workerPool this.consensus = options.consensus + this.blockHasher = options.blockHasher } /** @@ -97,13 +101,8 @@ export class Strategy { return this.workerPool.createMinersFee(minerSpendKey, amount, '', transactionVersion) } - hashHeader(header: RawBlockHeader): BlockHash { - const serialized = serializeHeaderBlake3(header) - return blake3(serialized) - } - newBlockHeader(raw: RawBlockHeader, noteSize?: number | null, work?: bigint): BlockHeader { - const hash = this.hashHeader(raw) + const hash = this.blockHasher.hashHeader(raw) return new BlockHeader(raw, hash, noteSize, work) } @@ -112,17 +111,3 @@ export class Strategy { return new Block(header, raw.transactions) } } - -function serializeHeaderBlake3(header: RawBlockHeader): Buffer { - const bw = bufio.write(180) - bw.writeBigU64BE(header.randomness) - bw.writeU32(header.sequence) - bw.writeHash(header.previousBlockHash) - bw.writeHash(header.noteCommitment) - bw.writeHash(header.transactionCommitment) - bw.writeBigU256BE(header.target.asBigInt()) - bw.writeU64(header.timestamp.getTime()) - bw.writeBytes(header.graffiti) - - return bw.render() -} diff --git a/ironfish/src/testUtilities/nodeTest.ts b/ironfish/src/testUtilities/nodeTest.ts index 0752a06b03..e8a26d1cb9 100644 --- a/ironfish/src/testUtilities/nodeTest.ts +++ b/ironfish/src/testUtilities/nodeTest.ts @@ -2,6 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import './matchers' +import { FishHashContext } from '@ironfish/rust-nodejs' import { Blockchain } from '../blockchain' import { Verifier } from '../consensus/verifier' import { ConfigOptions } from '../fileStores/config' @@ -21,6 +22,9 @@ export type NodeTestOptions = } | undefined +// Create global FishHash context for tests +export const FISH_HASH_CONTEXT = new FishHashContext(false) + /** * Used as an easy wrapper for testing the node, and blockchain. Use * {@link createNodeTest} to create one to make sure you call the proper @@ -90,7 +94,10 @@ export class NodeTest { } } - const node = await sdk.node({ autoSeed: this.options?.autoSeed }) + const node = await sdk.node({ + autoSeed: this.options?.autoSeed, + fishHashContext: FISH_HASH_CONTEXT, + }) const strategy = node.strategy as TestStrategy const chain = node.chain const wallet = node.wallet From df29d10f12ddb64c8a00b5decb6e5f8873961ead Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Thu, 18 Jan 2024 10:32:29 -0800 Subject: [PATCH 13/38] Rahul/move split secret (#4557) * moving split secret to frost_utils * removing unused import * removing double & * source code heading --- ironfish-rust/src/frost_utils/mod.rs | 1 + ironfish-rust/src/frost_utils/round_one.rs | 3 +- ironfish-rust/src/frost_utils/split_secret.rs | 90 +++++++++++++++++++ ironfish-rust/src/transaction/mod.rs | 54 +---------- ironfish-rust/src/transaction/tests.rs | 36 +------- 5 files changed, 95 insertions(+), 89 deletions(-) create mode 100644 ironfish-rust/src/frost_utils/split_secret.rs diff --git a/ironfish-rust/src/frost_utils/mod.rs b/ironfish-rust/src/frost_utils/mod.rs index 25bb98d3e2..18071df78d 100644 --- a/ironfish-rust/src/frost_utils/mod.rs +++ b/ironfish-rust/src/frost_utils/mod.rs @@ -3,3 +3,4 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ pub mod round_one; +pub mod split_secret; diff --git a/ironfish-rust/src/frost_utils/round_one.rs b/ironfish-rust/src/frost_utils/round_one.rs index 3b6c4177d1..344260d89d 100644 --- a/ironfish-rust/src/frost_utils/round_one.rs +++ b/ironfish-rust/src/frost_utils/round_one.rs @@ -17,13 +17,12 @@ pub fn round_one(key_package: &KeyPackage, seed: u64) -> (SigningNonces, Signing #[cfg(test)] mod test { - use ff::Field; use ironfish_frost::frost::keys::IdentifierList; use jubjub::Fr; use rand::rngs::ThreadRng; - use crate::transaction::{split_secret, SecretShareConfig}; + use crate::frost_utils::split_secret::{split_secret, SecretShareConfig}; #[test] pub fn test_seed_provides_same_result() { diff --git a/ironfish-rust/src/frost_utils/split_secret.rs b/ironfish-rust/src/frost_utils/split_secret.rs new file mode 100644 index 0000000000..81c4788c79 --- /dev/null +++ b/ironfish-rust/src/frost_utils/split_secret.rs @@ -0,0 +1,90 @@ +/* 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_frost::frost; +use ironfish_frost::frost::Error; +use ironfish_frost::frost::{ + keys::{IdentifierList, KeyPackage, PublicKeyPackage}, + Identifier, SigningKey, +}; +use rand::rngs::ThreadRng; +use std::collections::HashMap; + +pub struct SecretShareConfig { + pub min_signers: u16, + pub max_signers: u16, + pub secret: Vec, +} + +pub fn split_secret( + config: &SecretShareConfig, + identifiers: IdentifierList, + rng: &mut ThreadRng, +) -> Result<(HashMap, PublicKeyPackage), Error> { + let secret_key = SigningKey::deserialize( + config + .secret + .clone() + .try_into() + .map_err(|_| Error::MalformedSigningKey)?, + )?; + + let (shares, pubkeys) = frost::keys::split( + &secret_key, + config.max_signers, + config.min_signers, + identifiers, + rng, + )?; + + for (_k, v) in shares.clone() { + frost::keys::KeyPackage::try_from(v)?; + } + + let mut key_packages: HashMap<_, _> = HashMap::new(); + + for (identifier, secret_share) in shares { + let key_package = frost::keys::KeyPackage::try_from(secret_share.clone()).unwrap(); + key_packages.insert(identifier, key_package); + } + + Ok((key_packages, pubkeys)) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::keys::SaplingKey; + use ironfish_frost::frost::{frost::keys::reconstruct, JubjubBlake2b512}; + + #[test] + fn test_split_secret() { + let mut rng = rand::thread_rng(); + + let key = SaplingKey::generate_key().spend_authorizing_key.to_bytes(); + + let config = SecretShareConfig { + min_signers: 2, + max_signers: 3, + secret: key.to_vec(), + }; + + let (key_packages, _) = split_secret( + &config, + ironfish_frost::frost::keys::IdentifierList::Default, + &mut rng, + ) + .unwrap(); + assert_eq!(key_packages.len(), 3); + + let key_parts: Vec<_> = key_packages.values().cloned().collect(); + + let signing_key = + reconstruct::(&key_parts).expect("key reconstruction failed"); + + let scalar = signing_key.to_scalar(); + + assert_eq!(scalar.to_bytes(), key); + } +} diff --git a/ironfish-rust/src/transaction/mod.rs b/ironfish-rust/src/transaction/mod.rs index cdfc04380c..38912549f0 100644 --- a/ironfish-rust/src/transaction/mod.rs +++ b/ironfish-rust/src/transaction/mod.rs @@ -21,17 +21,7 @@ use crate::{ OutgoingViewKey, OutputDescription, SpendDescription, ViewKey, }; -use ironfish_frost::frost; -use ironfish_frost::frost::Error; -use rand::{ - rngs::{OsRng, ThreadRng}, - thread_rng, -}; - -use ironfish_frost::frost::{ - keys::{IdentifierList, KeyPackage, PublicKeyPackage}, - Identifier, SigningKey, -}; +use rand::{rngs::OsRng, thread_rng}; use bellperson::groth16::{verify_proofs_batch, PreparedVerifyingKey}; use blake2b_simd::Params as Blake2b; @@ -49,7 +39,6 @@ use ironfish_zkp::{ }; use std::{ - collections::HashMap, io::{self, Write}, iter, slice::Iter, @@ -947,44 +936,3 @@ pub fn batch_verify_transactions<'a>( &SAPLING.mint_verifying_key, ) } - -pub struct SecretShareConfig { - pub min_signers: u16, - pub max_signers: u16, - pub secret: Vec, -} - -pub fn split_secret( - config: &SecretShareConfig, - identifiers: IdentifierList, - rng: &mut ThreadRng, -) -> Result<(HashMap, PublicKeyPackage), Error> { - let secret_key = SigningKey::deserialize( - config - .secret - .clone() - .try_into() - .map_err(|_| Error::MalformedSigningKey)?, - )?; - - let (shares, pubkeys) = frost::keys::split( - &secret_key, - config.max_signers, - config.min_signers, - identifiers, - rng, - )?; - - for (_k, v) in shares.clone() { - frost::keys::KeyPackage::try_from(v)?; - } - - let mut key_packages: HashMap<_, _> = HashMap::new(); - - for (identifier, secret_share) in shares { - let key_package = frost::keys::KeyPackage::try_from(secret_share.clone()).unwrap(); - key_packages.insert(identifier, key_package); - } - - Ok((key_packages, pubkeys)) -} diff --git a/ironfish-rust/src/transaction/tests.rs b/ironfish-rust/src/transaction/tests.rs index 6c65a3601e..a8f79fa9da 100644 --- a/ironfish-rust/src/transaction/tests.rs +++ b/ironfish-rust/src/transaction/tests.rs @@ -14,14 +14,12 @@ use crate::{ sapling_bls12::SAPLING, test_util::make_fake_witness, transaction::{ - batch_verify_transactions, split_secret, verify_transaction, SecretShareConfig, - TransactionVersion, TRANSACTION_EXPIRATION_SIZE, TRANSACTION_FEE_SIZE, - TRANSACTION_SIGNATURE_SIZE, + batch_verify_transactions, verify_transaction, TransactionVersion, + TRANSACTION_EXPIRATION_SIZE, TRANSACTION_FEE_SIZE, TRANSACTION_SIGNATURE_SIZE, }, }; use ff::Field; -use ironfish_frost::frost::{frost::keys::reconstruct, JubjubBlake2b512}; use ironfish_zkp::{ constants::{ASSET_ID_LENGTH, SPENDING_KEY_GENERATOR, TREE_DEPTH}, proofs::{MintAsset, Output, Spend}, @@ -645,33 +643,3 @@ fn test_batch_verify() { Err(e) if matches!(e.kind, IronfishErrorKind::InvalidSpendSignature) )); } - -#[test] -fn test_split_secret() { - let mut rng = rand::thread_rng(); - - let key = SaplingKey::generate_key().spend_authorizing_key.to_bytes(); - - let config = SecretShareConfig { - min_signers: 2, - max_signers: 3, - secret: key.to_vec(), - }; - - let (key_packages, _) = split_secret( - &config, - ironfish_frost::frost::keys::IdentifierList::Default, - &mut rng, - ) - .unwrap(); - assert_eq!(key_packages.len(), 3); - - let key_parts: Vec<_> = key_packages.values().cloned().collect(); - - let signing_key = - reconstruct::(&key_parts).expect("key reconstruction failed"); - - let scalar = signing_key.to_scalar(); - - assert_eq!(scalar.to_bytes(), key); -} From dbbab1b11b416ebd6f33f196695bf5baa09f0604 Mon Sep 17 00:00:00 2001 From: jowparks Date: Thu, 18 Jan 2024 10:39:21 -0800 Subject: [PATCH 14/38] napi binding for round one of frost signing (#4556) --- ironfish-rust-nodejs/src/frost.rs | 35 +++++++++++++++++++++++++++++++ ironfish-rust-nodejs/src/lib.rs | 1 + 2 files changed, 36 insertions(+) create mode 100644 ironfish-rust-nodejs/src/frost.rs diff --git a/ironfish-rust-nodejs/src/frost.rs b/ironfish-rust-nodejs/src/frost.rs new file mode 100644 index 0000000000..f43e339a4f --- /dev/null +++ b/ironfish-rust-nodejs/src/frost.rs @@ -0,0 +1,35 @@ +/* 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::{ + frost::keys::KeyPackage, + frost_utils::round_one::round_one as round_one_rust, + serializing::{bytes_to_hex, hex_to_vec_bytes}, +}; +use napi::bindgen_prelude::*; +use napi_derive::napi; + +use crate::to_napi_err; + +#[napi(object)] +pub struct RoundOneSigningData { + pub nonce_hiding: String, + pub nonce_binding: String, + pub commitment_hiding: String, + pub commitment_binding: String, +} + +#[napi] +pub fn round_one(key_package: String, seed: u32) -> Result { + let key_package = + KeyPackage::deserialize(&hex_to_vec_bytes(&key_package).map_err(to_napi_err)?) + .map_err(to_napi_err)?; + let (nonce, commitment) = round_one_rust(&key_package, seed as u64); + Ok(RoundOneSigningData { + nonce_hiding: bytes_to_hex(&nonce.hiding().serialize()), + nonce_binding: bytes_to_hex(&nonce.binding().serialize()), + commitment_hiding: bytes_to_hex(&commitment.hiding().serialize()), + commitment_binding: bytes_to_hex(&commitment.binding().serialize()), + }) +} diff --git a/ironfish-rust-nodejs/src/lib.rs b/ironfish-rust-nodejs/src/lib.rs index bfb186e9a3..57e481069b 100644 --- a/ironfish-rust-nodejs/src/lib.rs +++ b/ironfish-rust-nodejs/src/lib.rs @@ -14,6 +14,7 @@ use ironfish::mining; use ironfish::sapling_bls12; pub mod fish_hash; +pub mod frost; pub mod mpc; pub mod nacl; pub mod rolling_filter; From 44422d2e262396ea754bf1d2ca344d41404113d3 Mon Sep 17 00:00:00 2001 From: jowparks Date: Thu, 18 Jan 2024 11:32:13 -0800 Subject: [PATCH 15/38] round_two signing that produces signature share after cooridnator assembles commitments from participants (#4558) --- ironfish-rust/src/errors.rs | 1 + ironfish-rust/src/frost_utils/mod.rs | 1 + ironfish-rust/src/frost_utils/round_two.rs | 27 ++++++++++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 ironfish-rust/src/frost_utils/round_two.rs diff --git a/ironfish-rust/src/errors.rs b/ironfish-rust/src/errors.rs index ae0eeec408..90c809def3 100644 --- a/ironfish-rust/src/errors.rs +++ b/ironfish-rust/src/errors.rs @@ -57,6 +57,7 @@ pub enum IronfishErrorKind { Io, IsSmallOrder, RandomnessError, + RoundTwoSigningFailure, TryFromInt, Utf8, } diff --git a/ironfish-rust/src/frost_utils/mod.rs b/ironfish-rust/src/frost_utils/mod.rs index 18071df78d..fdc67c6e20 100644 --- a/ironfish-rust/src/frost_utils/mod.rs +++ b/ironfish-rust/src/frost_utils/mod.rs @@ -3,4 +3,5 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ pub mod round_one; +pub mod round_two; pub mod split_secret; diff --git a/ironfish-rust/src/frost_utils/round_two.rs b/ironfish-rust/src/frost_utils/round_two.rs new file mode 100644 index 0000000000..24db90b2d3 --- /dev/null +++ b/ironfish-rust/src/frost_utils/round_two.rs @@ -0,0 +1,27 @@ +/* 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_frost::frost::{ + self, + keys::KeyPackage, + round1::SigningNonces, + round2::{Randomizer, SignatureShare}, + SigningPackage, +}; +use rand::{rngs::StdRng, SeedableRng}; + +use crate::errors::{IronfishError, IronfishErrorKind}; + +// Wrapper around frost::round2::sign that provides a seedable rng from u64 +pub fn round_two( + signing_package: SigningPackage, + key_package: KeyPackage, + randomizer: Randomizer, + seed: u64, +) -> Result { + let mut rng = StdRng::seed_from_u64(seed); + let signer_nonces = SigningNonces::new(key_package.signing_share(), &mut rng); + frost::round2::sign(&signing_package, &signer_nonces, &key_package, randomizer) + .map_err(|_| IronfishError::new(IronfishErrorKind::RoundTwoSigningFailure)) +} From 79c62d4daa9c3f122e63374a8caff55d73a278dd Mon Sep 17 00:00:00 2001 From: jowparks Date: Thu, 18 Jan 2024 12:41:58 -0800 Subject: [PATCH 16/38] adds round_two napi binding for frost signing (#4559) --- ironfish-rust-nodejs/src/frost.rs | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/ironfish-rust-nodejs/src/frost.rs b/ironfish-rust-nodejs/src/frost.rs index f43e339a4f..bb37b8b088 100644 --- a/ironfish-rust-nodejs/src/frost.rs +++ b/ironfish-rust-nodejs/src/frost.rs @@ -3,9 +3,13 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use ironfish::{ - frost::keys::KeyPackage, - frost_utils::round_one::round_one as round_one_rust, - serializing::{bytes_to_hex, hex_to_vec_bytes}, + frost::{ + keys::KeyPackage, + round2::{Randomizer, SignatureShare}, + SigningPackage, + }, + frost_utils::{round_one::round_one as round_one_rust, round_two::round_two as round_two_rust}, + serializing::{bytes_to_hex, hex_to_bytes, hex_to_vec_bytes}, }; use napi::bindgen_prelude::*; use napi_derive::napi; @@ -33,3 +37,21 @@ pub fn round_one(key_package: String, seed: u32) -> Result commitment_binding: bytes_to_hex(&commitment.binding().serialize()), }) } + +pub fn round_two( + signing_package: String, + key_package: String, + public_key_randomness: String, + seed: u64, +) -> Result { + let key_package = + KeyPackage::deserialize(&hex_to_vec_bytes(&key_package).map_err(to_napi_err)?[..]) + .map_err(to_napi_err)?; + let signing_package = + SigningPackage::deserialize(&hex_to_vec_bytes(&signing_package).map_err(to_napi_err)?[..]) + .map_err(to_napi_err)?; + let randomizer = + Randomizer::deserialize(&hex_to_bytes(&public_key_randomness).map_err(to_napi_err)?) + .map_err(to_napi_err)?; + round_two_rust(signing_package, key_package, randomizer, seed).map_err(to_napi_err) +} From a759728529828d736f7dddfcbc84bb1cc9374c2c Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Thu, 18 Jan 2024 16:06:50 -0500 Subject: [PATCH 17/38] Load or generate networkIdentity if none is passed (#4540) --- ironfish-cli/src/commands/start.ts | 8 +- ironfish/src/network/identity.ts | 12 +++ ironfish/src/node.ts | 20 +++- ironfish/src/sdk.test.ts | 152 ++++++++++++++++++++++++++++- ironfish/src/sdk.ts | 8 +- 5 files changed, 181 insertions(+), 19 deletions(-) diff --git a/ironfish-cli/src/commands/start.ts b/ironfish-cli/src/commands/start.ts index 0b2d63c0ed..d89d13eb31 100644 --- a/ironfish-cli/src/commands/start.ts +++ b/ironfish-cli/src/commands/start.ts @@ -213,7 +213,7 @@ export default class Start extends IronfishCommand { await this.sdk.internal.save() } - const node = await this.sdk.node({ privateIdentity: this.sdk.getPrivateIdentity() }) + const node = await this.sdk.node() const nodeName = this.sdk.config.get('nodeName').trim() || null const blockGraffiti = this.sdk.config.get('blockGraffiti').trim() || null @@ -254,12 +254,6 @@ export default class Start extends IronfishCommand { this.exit(1) } - const newSecretKey = Buffer.from( - node.peerNetwork.localPeer.privateIdentity.secretKey, - ).toString('hex') - node.internal.set('networkIdentity', newSecretKey) - await node.internal.save() - if (node.internal.get('isFirstRun')) { await this.firstRun(node) } diff --git a/ironfish/src/network/identity.ts b/ironfish/src/network/identity.ts index 1195565150..bdae391ce9 100644 --- a/ironfish/src/network/identity.ts +++ b/ironfish/src/network/identity.ts @@ -33,6 +33,18 @@ export const secretKeyLength = KEY_LENGTH */ export const base64IdentityLength = Math.ceil(identityLength / 3) * 4 +/** + * Length of the secret key as a hex-encoded string. + */ +export const hexSecretKeyLength = secretKeyLength * 2 + +export function isHexSecretKey(obj: string): boolean { + return ( + obj.length === hexSecretKeyLength && + Buffer.from(obj, 'hex').toString('hex').toLowerCase() === obj.toLowerCase() + ) +} + export function isIdentity(obj: string): boolean { // Should be a base64-encoded string with the expected length return ( diff --git a/ironfish/src/node.ts b/ironfish/src/node.ts index 551b282116..b92082a29e 100644 --- a/ironfish/src/node.ts +++ b/ironfish/src/node.ts @@ -22,6 +22,7 @@ import { MetricsMonitor } from './metrics' import { Migrator } from './migrations' import { MiningManager } from './mining' import { PeerNetwork, PrivateIdentity, privateIdentityToIdentity } from './network' +import { isHexSecretKey } from './network/identity' import { IsomorphicWebSocketConstructor, NodeDataChannelType } from './network/types' import { getNetworkDefinition } from './networkDefinition' import { Package } from './package' @@ -90,7 +91,7 @@ export class FullNode { logger: Logger webSocket: IsomorphicWebSocketConstructor nodeDataChannel: NodeDataChannelType - privateIdentity?: PrivateIdentity + privateIdentity: PrivateIdentity peerStore: PeerStore networkId: number assetsVerifier: AssetsVerifier @@ -117,15 +118,13 @@ export class FullNode { this.migrator = new Migrator({ context: this, logger }) - const identity = privateIdentity || new BoxKeyPair() - this.telemetry = new Telemetry({ chain, logger, config, metrics, workerPool, - localPeerIdentity: privateIdentityToIdentity(identity), + localPeerIdentity: privateIdentityToIdentity(privateIdentity), defaultTags: [ { name: 'version', value: pkg.version }, { name: 'agent', value: Platform.getAgent(pkg) }, @@ -139,7 +138,7 @@ export class FullNode { this.peerNetwork = new PeerNetwork({ networkId, - identity: identity, + identity: privateIdentity, agent: Platform.getAgent(pkg), port: config.get('peerPort'), name: config.get('nodeName'), @@ -249,6 +248,17 @@ export class FullNode { config.setOverride('bootstrapNodes', networkDefinition.bootstrapNodes) } + if (config.get('generateNewIdentity')) { + privateIdentity = new BoxKeyPair() + } else if (!privateIdentity) { + const internalNetworkIdentity = internal.get('networkIdentity') + privateIdentity = isHexSecretKey(internalNetworkIdentity) + ? BoxKeyPair.fromHex(internalNetworkIdentity) + : new BoxKeyPair() + } + internal.set('networkIdentity', privateIdentity.secretKey.toString('hex')) + await internal.save() + const consensus = new TestnetConsensus(networkDefinition.consensus) // TODO: config value for fullContext const blockHasher = new BlockHasher({ diff --git a/ironfish/src/sdk.test.ts b/ironfish/src/sdk.test.ts index 5661e7a419..f8d5900d7b 100644 --- a/ironfish/src/sdk.test.ts +++ b/ironfish/src/sdk.test.ts @@ -1,8 +1,9 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { BoxKeyPair } from '@ironfish/rust-nodejs' import { Assert } from './assert' -import { Config, DEFAULT_DATA_DIR } from './fileStores' +import { Config, DEFAULT_DATA_DIR, InternalStore } from './fileStores' import { NodeFileProvider } from './fileSystems' import { FullNode } from './node' import { Platform } from './platform' @@ -106,6 +107,155 @@ describe('IronfishSdk', () => { }) }) + describe('node', () => { + describe('networkIdentity', () => { + const saveInternalIdentity = async ( + fileSystem: NodeFileProvider, + dataDir: string, + identity: string, + ) => { + const internal = new InternalStore(fileSystem, dataDir) + await internal.load() + expect(internal.isSet('networkIdentity')).toBe(false) + internal.set('networkIdentity', identity) + await internal.save() + } + + it('should initialize networkIdentity if none is passed in', async () => { + const fileSystem = new NodeFileProvider() + await fileSystem.init() + + const dataDir = getUniqueTestDataDir() + + const internal = new InternalStore(fileSystem, dataDir) + await internal.load() + expect(internal.isSet('networkIdentity')).toBe(false) + + const sdk = await IronfishSdk.init({ + dataDir: dataDir, + fileSystem: fileSystem, + }) + + const node = await sdk.node() + + const peerNetworkIdentity = + node.peerNetwork.localPeer.privateIdentity.secretKey.toString('hex') + expect(sdk.internal.isSet('networkIdentity')).toBe(true) + expect(sdk.internal.get('networkIdentity')).toEqual(peerNetworkIdentity) + }) + + it('should load networkIdentity if one is saved', async () => { + const fileSystem = new NodeFileProvider() + await fileSystem.init() + + const dataDir = getUniqueTestDataDir() + const identity = new BoxKeyPair().secretKey.toString('hex') + await saveInternalIdentity(fileSystem, dataDir, identity) + + const sdk = await IronfishSdk.init({ + dataDir: dataDir, + fileSystem: fileSystem, + }) + + const node = await sdk.node() + + const peerNetworkIdentity = + node.peerNetwork.localPeer.privateIdentity.secretKey.toString('hex') + expect(sdk.internal.isSet('networkIdentity')).toBe(true) + expect(sdk.internal.get('networkIdentity')).toEqual(peerNetworkIdentity) + expect(identity).toEqual(peerNetworkIdentity) + }) + + it('should save a new networkIdentity if generateNewIdentity is set', async () => { + const fileSystem = new NodeFileProvider() + await fileSystem.init() + + const dataDir = getUniqueTestDataDir() + const identity = new BoxKeyPair().secretKey.toString('hex') + await saveInternalIdentity(fileSystem, dataDir, identity) + + const sdk = await IronfishSdk.init({ + dataDir: dataDir, + fileSystem: fileSystem, + configOverrides: { + generateNewIdentity: true, + }, + }) + + const node = await sdk.node() + + const peerNetworkIdentity = + node.peerNetwork.localPeer.privateIdentity.secretKey.toString('hex') + expect(sdk.internal.isSet('networkIdentity')).toBe(true) + expect(sdk.internal.get('networkIdentity')).toEqual(peerNetworkIdentity) + expect(identity).not.toEqual(peerNetworkIdentity) + }) + + it('should override and save networkIdentity if one is passed in', async () => { + const fileSystem = new NodeFileProvider() + await fileSystem.init() + + const dataDir = getUniqueTestDataDir() + const savedIdentity = new BoxKeyPair().secretKey.toString('hex') + const overrideIdentity = new BoxKeyPair() + await saveInternalIdentity(fileSystem, dataDir, savedIdentity) + + const sdk = await IronfishSdk.init({ + dataDir: dataDir, + fileSystem: fileSystem, + }) + + const node = await sdk.node({ privateIdentity: overrideIdentity }) + + const newInternal = new InternalStore(fileSystem, dataDir) + await newInternal.load() + expect(newInternal.get('networkIdentity')).toEqual( + overrideIdentity.secretKey.toString('hex'), + ) + + const peerNetworkIdentity = + node.peerNetwork.localPeer.privateIdentity.secretKey.toString('hex') + expect(sdk.internal.isSet('networkIdentity')).toBe(true) + expect(sdk.internal.get('networkIdentity')).toEqual(peerNetworkIdentity) + expect(overrideIdentity.secretKey.toString('hex')).toEqual(peerNetworkIdentity) + }) + + it('should save a new networkIdentity if generateNewIdentity is set and one is passed in', async () => { + const fileSystem = new NodeFileProvider() + await fileSystem.init() + + const dataDir = getUniqueTestDataDir() + const savedIdentity = new BoxKeyPair().secretKey.toString('hex') + const overrideIdentity = new BoxKeyPair() + await saveInternalIdentity(fileSystem, dataDir, savedIdentity) + + const sdk = await IronfishSdk.init({ + dataDir: dataDir, + fileSystem: fileSystem, + configOverrides: { + generateNewIdentity: true, + }, + }) + + const node = await sdk.node({ privateIdentity: overrideIdentity }) + + const newInternal = new InternalStore(fileSystem, dataDir) + await newInternal.load() + const generatedIdentity = newInternal.get('networkIdentity') + expect(generatedIdentity).not.toEqual(savedIdentity) + expect(generatedIdentity).not.toEqual(overrideIdentity.secretKey.toString('hex')) + + const peerNetworkIdentity = + node.peerNetwork.localPeer.privateIdentity.secretKey.toString('hex') + expect(sdk.internal.isSet('networkIdentity')).toBe(true) + expect(sdk.internal.get('networkIdentity')).toEqual(peerNetworkIdentity) + expect(peerNetworkIdentity).toEqual(generatedIdentity) + expect(overrideIdentity.secretKey.toString('hex')).not.toEqual(generatedIdentity) + expect(savedIdentity).not.toEqual(generatedIdentity) + }) + }) + }) + describe('connectRpc', () => { describe('when local is true', () => { it('returns and connects `clientMemory` to a node', async () => { diff --git a/ironfish/src/sdk.ts b/ironfish/src/sdk.ts index 27a79144d3..7202909c42 100644 --- a/ironfish/src/sdk.ts +++ b/ironfish/src/sdk.ts @@ -20,7 +20,7 @@ import { } from './logger' import { FileReporter } from './logger/reporters' import { MetricsMonitor } from './metrics' -import { PrivateIdentity } from './network/identity' +import { isHexSecretKey, PrivateIdentity } from './network/identity' import { IsomorphicWebSocketConstructor } from './network/types' import { WebSocketClient } from './network/webSocketClient' import { FullNode } from './node' @@ -272,11 +272,7 @@ export class IronfishSdk { getPrivateIdentity(): PrivateIdentity | undefined { const networkIdentity = this.internal.get('networkIdentity') - if ( - !this.config.get('generateNewIdentity') && - networkIdentity !== undefined && - networkIdentity.length > 31 - ) { + if (!this.config.get('generateNewIdentity') && isHexSecretKey(networkIdentity)) { return BoxKeyPair.fromHex(networkIdentity) } } From 6a757a0ef482d759d631e739f06ff7142c891a96 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Thu, 18 Jan 2024 15:23:40 -0800 Subject: [PATCH 18/38] adds multiSigKeys to account imports, exports (#4560) * adds multiSigKeys to account imports, exports updates bech32 and json account encoders adds multiSigKeys to RpcAccountImport increases account version * fixes import test --- .../rpc/routes/wallet/exportAccount.test.ts | 46 +++++++++++++++++++ .../rpc/routes/wallet/importAccount.test.ts | 32 +++++++++++++ ironfish/src/rpc/routes/wallet/types.ts | 12 +++++ ironfish/src/wallet/account/account.ts | 3 +- .../src/wallet/account/encoder/bech32.test.ts | 24 ++++++++++ ironfish/src/wallet/account/encoder/bech32.ts | 25 ++++++++++ .../src/wallet/account/encoder/json.test.ts | 29 ++++++++++++ 7 files changed, 170 insertions(+), 1 deletion(-) diff --git a/ironfish/src/rpc/routes/wallet/exportAccount.test.ts b/ironfish/src/rpc/routes/wallet/exportAccount.test.ts index 165c585db1..7d95c24eba 100644 --- a/ironfish/src/rpc/routes/wallet/exportAccount.test.ts +++ b/ironfish/src/rpc/routes/wallet/exportAccount.test.ts @@ -2,6 +2,8 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { generateKey } from '@ironfish/rust-nodejs' +import { v4 as uuid } from 'uuid' import { useAccountFixture } from '../../../testUtilities' import { createRouteTest } from '../../../testUtilities/routeTest' import { Account } from '../../../wallet' @@ -138,4 +140,48 @@ describe('Route wallet/exportAccount', () => { .waitForEnd(), ).rejects.toThrow() }) + + it('should export an account with multiSigKeys', async () => { + const key = generateKey() + + const accountName = 'foo' + const accountImport = { + name: accountName, + viewKey: key.viewKey, + spendingKey: null, + publicAddress: key.publicAddress, + incomingViewKey: key.incomingViewKey, + outgoingViewKey: key.outgoingViewKey, + version: 1, + createdAt: null, + multiSigKeys: { + identifier: 'aaaa', + keyPackage: 'bbbb', + proofGenerationKey: 'cccc', + }, + } + + await routeTest.wallet.importAccount({ ...accountImport, id: uuid() }) + + const response = await routeTest.client + .request('wallet/exportAccount', { + account: accountName, + viewOnly: false, + }) + .waitForEnd() + + expect(response.status).toBe(200) + expect(response.content).toMatchObject({ + account: { + name: accountImport.name, + spendingKey: accountImport.spendingKey, + viewKey: accountImport.viewKey, + incomingViewKey: accountImport.incomingViewKey, + outgoingViewKey: accountImport.outgoingViewKey, + publicAddress: accountImport.publicAddress, + version: accountImport.version, + multiSigKeys: accountImport.multiSigKeys, + }, + }) + }) }) diff --git a/ironfish/src/rpc/routes/wallet/importAccount.test.ts b/ironfish/src/rpc/routes/wallet/importAccount.test.ts index 0556ff02d1..199fab08b2 100644 --- a/ironfish/src/rpc/routes/wallet/importAccount.test.ts +++ b/ironfish/src/rpc/routes/wallet/importAccount.test.ts @@ -48,6 +48,38 @@ describe('Route wallet/importAccount', () => { }) }) + it('should import a multisig account that has no spending key', async () => { + const key = generateKey() + + const accountName = 'multisig' + const response = await routeTest.client + .request('wallet/importAccount', { + account: { + name: accountName, + viewKey: key.viewKey, + spendingKey: null, + publicAddress: key.publicAddress, + incomingViewKey: key.incomingViewKey, + outgoingViewKey: key.outgoingViewKey, + version: 1, + createdAt: null, + multiSigKeys: { + identifier: 'aaaa', + keyPackage: 'bbbb', + proofGenerationKey: 'cccc', + }, + }, + rescan: false, + }) + .waitForEnd() + + expect(response.status).toBe(200) + expect(response.content).toMatchObject({ + name: accountName, + isDefaultAccount: false, + }) + }) + it('should import a spending account', async () => { const key = generateKey() diff --git a/ironfish/src/rpc/routes/wallet/types.ts b/ironfish/src/rpc/routes/wallet/types.ts index 19efff7939..85f0bec9c7 100644 --- a/ironfish/src/rpc/routes/wallet/types.ts +++ b/ironfish/src/rpc/routes/wallet/types.ts @@ -142,6 +142,11 @@ export type RpcAccountImport = { publicAddress: string spendingKey: string | null createdAt: { hash: string; sequence: number } | null + multiSigKeys?: { + identifier: string + keyPackage: string + proofGenerationKey: string + } } export const RpcAccountImportSchema: yup.ObjectSchema = yup @@ -160,6 +165,13 @@ export const RpcAccountImportSchema: yup.ObjectSchema = yup }) .nullable() .defined(), + multiSigKeys: yup + .object({ + identifier: yup.string().defined(), + keyPackage: yup.string().defined(), + proofGenerationKey: yup.string().defined(), + }) + .optional(), }) .defined() diff --git a/ironfish/src/wallet/account/account.ts b/ironfish/src/wallet/account/account.ts index c009c748c5..2ed4b42d82 100644 --- a/ironfish/src/wallet/account/account.ts +++ b/ironfish/src/wallet/account/account.ts @@ -24,7 +24,7 @@ import { WalletDB } from '../walletdb/walletdb' export const ACCOUNT_KEY_LENGTH = 32 -export const ACCOUNT_SCHEMA_VERSION = 2 +export const ACCOUNT_SCHEMA_VERSION = 3 export type SpendingAccount = WithNonNull @@ -100,6 +100,7 @@ export class Account { outgoingViewKey: this.outgoingViewKey, publicAddress: this.publicAddress, createdAt: this.createdAt, + multiSigKeys: this.multiSigKeys, } } diff --git a/ironfish/src/wallet/account/encoder/bech32.test.ts b/ironfish/src/wallet/account/encoder/bech32.test.ts index 8833950210..1f635cef36 100644 --- a/ironfish/src/wallet/account/encoder/bech32.test.ts +++ b/ironfish/src/wallet/account/encoder/bech32.test.ts @@ -55,6 +55,30 @@ describe('Bech32AccountEncoder', () => { expect(decoded).toMatchObject(accountImport) }) + it('encodes and decodes accounts with multisig keys', () => { + const accountImport: AccountImport = { + version: ACCOUNT_SCHEMA_VERSION, + name: 'test', + spendingKey: null, + viewKey: key.viewKey, + incomingViewKey: key.incomingViewKey, + outgoingViewKey: key.outgoingViewKey, + publicAddress: key.publicAddress, + createdAt: null, + multiSigKeys: { + identifier: 'aaaa', + keyPackage: 'bbbb', + proofGenerationKey: 'cccc', + }, + } + + const encoded = encoder.encode(accountImport) + expect(encoded.startsWith(BECH32_ACCOUNT_PREFIX)).toBe(true) + + const decoded = encoder.decode(encoded) + expect(decoded).toMatchObject(accountImport) + }) + it('encodes and decodes view-only accounts', () => { const accountImport: AccountImport = { version: ACCOUNT_SCHEMA_VERSION, diff --git a/ironfish/src/wallet/account/encoder/bech32.ts b/ironfish/src/wallet/account/encoder/bech32.ts index f23d79ef0c..e5efa7293f 100644 --- a/ironfish/src/wallet/account/encoder/bech32.ts +++ b/ironfish/src/wallet/account/encoder/bech32.ts @@ -33,6 +33,13 @@ export class Bech32Encoder implements AccountEncoder { bw.writeU32(value.createdAt.sequence) } + bw.writeU8(Number(!!value.multiSigKeys)) + if (value.multiSigKeys) { + bw.writeVarBytes(Buffer.from(value.multiSigKeys.identifier, 'hex')) + bw.writeVarBytes(Buffer.from(value.multiSigKeys.keyPackage, 'hex')) + bw.writeVarBytes(Buffer.from(value.multiSigKeys.proofGenerationKey, 'hex')) + } + return Bech32m.encode(bw.render().toString('hex'), BECH32_ACCOUNT_PREFIX) } @@ -53,6 +60,7 @@ export class Bech32Encoder implements AccountEncoder { let publicAddress: string let spendingKey: string | null let createdAt = null + let multiSigKeys = undefined try { const buffer = Buffer.from(hexEncoding, 'hex') @@ -83,6 +91,16 @@ export class Bech32Encoder implements AccountEncoder { const sequence = reader.readU32() createdAt = { hash, sequence } } + + const hasMultiSigKeys = reader.readU8() === 1 + + if (hasMultiSigKeys) { + multiSigKeys = { + identifier: reader.readVarBytes().toString('hex'), + keyPackage: reader.readVarBytes().toString('hex'), + proofGenerationKey: reader.readVarBytes().toString('hex'), + } + } } catch (e) { if (e instanceof EncodingError) { throw new DecodeFailed( @@ -102,6 +120,7 @@ export class Bech32Encoder implements AccountEncoder { spendingKey, publicAddress, createdAt, + multiSigKeys, } } @@ -122,6 +141,12 @@ export class Bech32Encoder implements AccountEncoder { size += 32 // block hash size += 4 // block sequence } + size += 1 // multiSigKeys byte + if (value.multiSigKeys) { + size += bufio.sizeVarBytes(Buffer.from(value.multiSigKeys.identifier, 'hex')) + size += bufio.sizeVarBytes(Buffer.from(value.multiSigKeys.keyPackage, 'hex')) + size += bufio.sizeVarBytes(Buffer.from(value.multiSigKeys.proofGenerationKey, 'hex')) + } return size } diff --git a/ironfish/src/wallet/account/encoder/json.test.ts b/ironfish/src/wallet/account/encoder/json.test.ts index e2cfecb7d1..7e9255430f 100644 --- a/ironfish/src/wallet/account/encoder/json.test.ts +++ b/ironfish/src/wallet/account/encoder/json.test.ts @@ -2,7 +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/. */ +import { generateKey } from '@ironfish/rust-nodejs' import { Assert } from '../../../assert' +import { AccountImport } from '../../walletdb/accountValue' +import { ACCOUNT_SCHEMA_VERSION } from '../account' import { JsonEncoder } from './json' describe('JsonEncoder', () => { describe('encoding/decoding', () => { @@ -28,5 +31,31 @@ describe('JsonEncoder', () => { Assert.isNotNull(decoded) expect(decoded.viewKey).not.toBeNull() }) + it('encodes and decodes accounts with multisig keys', () => { + const key = generateKey() + + const accountImport: AccountImport = { + version: ACCOUNT_SCHEMA_VERSION, + name: 'test', + spendingKey: null, + viewKey: key.viewKey, + incomingViewKey: key.incomingViewKey, + outgoingViewKey: key.outgoingViewKey, + publicAddress: key.publicAddress, + createdAt: null, + multiSigKeys: { + identifier: 'aaaa', + keyPackage: 'bbbb', + proofGenerationKey: 'cccc', + }, + } + + const encoder = new JsonEncoder() + + const encoded = encoder.encode(accountImport) + + const decoded = encoder.decode(encoded) + expect(decoded).toMatchObject(accountImport) + }) }) }) From 1497f22a840f3b7aec0a74d5b9a4274301bad8f7 Mon Sep 17 00:00:00 2001 From: Daniel Cogan Date: Thu, 18 Jan 2024 16:30:51 -0700 Subject: [PATCH 19/38] Add missing consensus params to chain/getConsensusParameters (#4566) --- .../chain/getConsensusParameters.test.ts | 22 ++++-------- .../routes/chain/getConsensusParameters.ts | 36 +++++++++---------- 2 files changed, 25 insertions(+), 33 deletions(-) diff --git a/ironfish/src/rpc/routes/chain/getConsensusParameters.test.ts b/ironfish/src/rpc/routes/chain/getConsensusParameters.test.ts index 83b31de1d9..eb476f23fe 100644 --- a/ironfish/src/rpc/routes/chain/getConsensusParameters.test.ts +++ b/ironfish/src/rpc/routes/chain/getConsensusParameters.test.ts @@ -13,21 +13,13 @@ describe('Route chain.getConsensusParameters', () => { .request('chain/getConsensusParameters') .waitForEnd() - expect(response.content.allowedBlockFuturesSeconds).toEqual( - routeTest.chain.consensus.parameters.allowedBlockFutureSeconds, + const chainParams = routeTest.chain.consensus.parameters + const expectedResponseParams = Object.fromEntries( + Object.entries(chainParams).map(([k, v]) => { + return [k, v === 'never' ? null : v] + }), ) - expect(response.content.genesisSupplyInIron).toEqual( - routeTest.chain.consensus.parameters.genesisSupplyInIron, - ) - expect(response.content.targetBlockTimeInSeconds).toEqual( - routeTest.chain.consensus.parameters.targetBlockTimeInSeconds, - ) - expect(response.content.targetBucketTimeInSeconds).toEqual( - routeTest.chain.consensus.parameters.targetBucketTimeInSeconds, - ) - expect(response.content.maxBlockSizeBytes).toEqual( - routeTest.chain.consensus.parameters.maxBlockSizeBytes, - ) - expect(response.content.minFee).toEqual(routeTest.chain.consensus.parameters.minFee) + + expect(response.content).toEqual(expectedResponseParams) }) }) diff --git a/ironfish/src/rpc/routes/chain/getConsensusParameters.ts b/ironfish/src/rpc/routes/chain/getConsensusParameters.ts index 7681b1724c..87c2f87a15 100644 --- a/ironfish/src/rpc/routes/chain/getConsensusParameters.ts +++ b/ironfish/src/rpc/routes/chain/getConsensusParameters.ts @@ -3,21 +3,17 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import * as yup from 'yup' import { Assert } from '../../../assert' +import { ActivationSequence, ConsensusParameters } from '../../../consensus/consensus' import { FullNode } from '../../../node' import { ApiNamespace } from '../namespaces' import { routes } from '../router' -interface ConsensusParameters { - allowedBlockFuturesSeconds: number - genesisSupplyInIron: number - targetBlockTimeInSeconds: number - targetBucketTimeInSeconds: number - maxBlockSizeBytes: number - minFee: number -} - export type GetConsensusParametersRequest = Record | undefined -export type GetConsensusParametersResponse = ConsensusParameters +export type GetConsensusParametersResponse = { + [K in keyof ConsensusParameters]: ConsensusParameters[K] extends number + ? ConsensusParameters[K] + : number | null +} export const GetConsensusParametersRequestSchema: yup.MixedSchema = yup.mixed().oneOf([undefined] as const) @@ -25,12 +21,15 @@ export const GetConsensusParametersRequestSchema: yup.MixedSchema = yup .object({ - allowedBlockFuturesSeconds: yup.number().defined(), + allowedBlockFutureSeconds: yup.number().defined(), genesisSupplyInIron: yup.number().defined(), targetBlockTimeInSeconds: yup.number().defined(), targetBucketTimeInSeconds: yup.number().defined(), maxBlockSizeBytes: yup.number().defined(), minFee: yup.number().defined(), + enableAssetOwnership: yup.number().nullable().defined(), + enforceSequentialBlockTime: yup.number().nullable().defined(), + enableFishHash: yup.number().nullable().defined(), }) .defined() @@ -39,17 +38,18 @@ routes.register { Assert.isInstanceOf(node, FullNode) - Assert.isNotNull(node.chain.consensus, 'no consensus parameters') const consensusParameters = node.chain.consensus.parameters + const neverToNull = (value: ActivationSequence): number | null => { + return value === 'never' ? null : value + } + request.end({ - allowedBlockFuturesSeconds: consensusParameters.allowedBlockFutureSeconds, - genesisSupplyInIron: consensusParameters.genesisSupplyInIron, - targetBlockTimeInSeconds: consensusParameters.targetBlockTimeInSeconds, - targetBucketTimeInSeconds: consensusParameters.targetBucketTimeInSeconds, - maxBlockSizeBytes: consensusParameters.maxBlockSizeBytes, - minFee: consensusParameters.minFee, + ...consensusParameters, + enableAssetOwnership: neverToNull(consensusParameters.enableAssetOwnership), + enforceSequentialBlockTime: neverToNull(consensusParameters.enforceSequentialBlockTime), + enableFishHash: neverToNull(consensusParameters.enableFishHash), }) }, ) From 0b5c60f7a65e6e1743763eabfb1eb26f71b1b5e1 Mon Sep 17 00:00:00 2001 From: Daniel Cogan Date: Thu, 18 Jan 2024 17:17:30 -0700 Subject: [PATCH 20/38] Add config value for building the full FishHash context (#4561) --- ironfish/src/blockHasher.ts | 15 ++++++--------- ironfish/src/consensus/consensus.ts | 7 +++++++ ironfish/src/fileStores/config.ts | 9 +++++++++ ironfish/src/node.ts | 16 ++++++++-------- ironfish/src/strategy.test.slow.ts | 29 +++++++---------------------- ironfish/src/strategy.test.ts | 7 ++----- ironfish/src/strategy.ts | 8 ++++++-- 7 files changed, 45 insertions(+), 46 deletions(-) diff --git a/ironfish/src/blockHasher.ts b/ironfish/src/blockHasher.ts index 89ac8daa94..9814a79d5a 100644 --- a/ironfish/src/blockHasher.ts +++ b/ironfish/src/blockHasher.ts @@ -13,16 +13,13 @@ export class BlockHasher { private readonly consensus: Consensus private readonly fishHashContext: FishHashContext | null = null - constructor(options: { - consensus: Consensus - fullContext?: boolean - context?: FishHashContext - }) { + constructor(options: { consensus: Consensus; context?: FishHashContext }) { this.consensus = options.consensus - if (this.consensus.parameters.enableFishHash !== 'never') { - this.fishHashContext = options.context - ? options.context - : new FishHashContext(!!options.fullContext) + + if (this.consensus.isNeverActive('enableFishHash')) { + this.fishHashContext = null + } else { + this.fishHashContext = options.context ?? new FishHashContext(false) } } diff --git a/ironfish/src/consensus/consensus.ts b/ironfish/src/consensus/consensus.ts index 8c5340d8ae..8d20e9206d 100644 --- a/ironfish/src/consensus/consensus.ts +++ b/ironfish/src/consensus/consensus.ts @@ -71,6 +71,13 @@ export class Consensus { return Math.max(1, sequence) >= upgrade } + /** + * Returns true if the upgrade can never activate on the network + */ + isNeverActive(upgrade: keyof ConsensusParameters): boolean { + return this.parameters[upgrade] === 'never' + } + getActiveTransactionVersion(sequence: number): TransactionVersion { if (this.isActive(this.parameters.enableAssetOwnership, sequence)) { return TransactionVersion.V2 diff --git a/ironfish/src/fileStores/config.ts b/ironfish/src/fileStores/config.ts index a90e263bbe..bdc8baeca2 100644 --- a/ironfish/src/fileStores/config.ts +++ b/ironfish/src/fileStores/config.ts @@ -298,6 +298,13 @@ export type ConfigOptions = { * The max number of transactions to process at one time when syncing */ walletSyncingMaxQueueSize: number + + /** + * Whether or not to build the full fish hash context at node startup. Setting this + * to `true` will slightly increase node performance but use ~4.5GB more RAM. The majority of + * network users will not need this speed increase and so should keep the default of `false`. + */ + fishHashFullContext: boolean } export const ConfigOptionsSchema: yup.ObjectSchema> = yup @@ -379,6 +386,7 @@ export const ConfigOptionsSchema: yup.ObjectSchema> = yup incomingWebSocketWhitelist: yup.array(yup.string().trim().defined()), walletGossipTransactionsMaxQueueSize: yup.number(), walletSyncingMaxQueueSize: yup.number(), + fishHashFullContext: yup.boolean(), }) .defined() @@ -486,6 +494,7 @@ export class Config< incomingWebSocketWhitelist: [], walletGossipTransactionsMaxQueueSize: 1000, walletSyncingMaxQueueSize: 100, + fishHashFullContext: false, } } } diff --git a/ironfish/src/node.ts b/ironfish/src/node.ts index b92082a29e..ad2b1f1007 100644 --- a/ironfish/src/node.ts +++ b/ironfish/src/node.ts @@ -5,7 +5,6 @@ import { BoxKeyPair, FishHashContext } from '@ironfish/rust-nodejs' import { v4 as uuid } from 'uuid' import { AssetsVerifier } from './assets' import { Blockchain } from './blockchain' -import { BlockHasher } from './blockHasher' import { TestnetConsensus } from './consensus' import { Config, @@ -260,15 +259,16 @@ export class FullNode { await internal.save() const consensus = new TestnetConsensus(networkDefinition.consensus) - // TODO: config value for fullContext - const blockHasher = new BlockHasher({ - consensus, - context: fishHashContext, - fullContext: false, - }) + + if (consensus.isNeverActive('enableFishHash')) { + fishHashContext = undefined + } else if (!fishHashContext) { + const isFull = config.get('fishHashFullContext') + fishHashContext = new FishHashContext(isFull) + } strategyClass = strategyClass || Strategy - const strategy = new strategyClass({ workerPool, consensus, blockHasher }) + const strategy = new strategyClass({ workerPool, consensus, fishHashContext }) const chain = new Blockchain({ location: config.chainDatabasePath, diff --git a/ironfish/src/strategy.test.slow.ts b/ironfish/src/strategy.test.slow.ts index 191bcf5034..c3039466e2 100644 --- a/ironfish/src/strategy.test.slow.ts +++ b/ironfish/src/strategy.test.slow.ts @@ -12,7 +12,7 @@ import { TransactionPosted as NativeTransactionPosted, } from '@ironfish/rust-nodejs' import { blake3 } from '@napi-rs/blake-hash' -import { BlockHasher, serializeHeaderBlake3, serializeHeaderFishHash } from './blockHasher' +import { serializeHeaderBlake3, serializeHeaderFishHash } from './blockHasher' import { ConsensusParameters, TestnetConsensus } from './consensus' import { MerkleTree } from './merkletree' import { LeafEncoding } from './merkletree/database/leaves' @@ -102,15 +102,10 @@ describe('Demonstrate the Sapling API', () => { spenderKey = generateKey() receiverKey = generateKey() workerPool = new WorkerPool() - const consensus = new TestnetConsensus(consensusParameters) - const blockHasher = new BlockHasher({ - consensus, - context: FISH_HASH_CONTEXT, - }) strategy = new Strategy({ workerPool, - consensus, - blockHasher, + consensus: new TestnetConsensus(consensusParameters), + fishHashContext: FISH_HASH_CONTEXT, }) }) @@ -262,15 +257,10 @@ describe('Demonstrate the Sapling API', () => { } const key = generateKey() - const modifiedConsensus = new TestnetConsensus(modifiedParams) - const blockHasher = new BlockHasher({ - consensus: modifiedConsensus, - context: FISH_HASH_CONTEXT, - }) const modifiedStrategy = new Strategy({ workerPool, - consensus: modifiedConsensus, - blockHasher, + consensus: new TestnetConsensus(modifiedParams), + fishHashContext: FISH_HASH_CONTEXT, }) const minersFee1 = await modifiedStrategy.createMinersFee( @@ -367,15 +357,10 @@ describe('Demonstrate the Sapling API', () => { } beforeAll(() => { - const modifiedConsensus = new TestnetConsensus(modifiedParams) - const blockHasher = new BlockHasher({ - consensus: modifiedConsensus, - context: FISH_HASH_CONTEXT, - }) modifiedStrategy = new Strategy({ workerPool, - consensus: modifiedConsensus, - blockHasher, + consensus: new TestnetConsensus(modifiedParams), + fishHashContext: FISH_HASH_CONTEXT, }) }) diff --git a/ironfish/src/strategy.test.ts b/ironfish/src/strategy.test.ts index 163613fc84..5140b20d72 100644 --- a/ironfish/src/strategy.test.ts +++ b/ironfish/src/strategy.test.ts @@ -2,7 +2,6 @@ * 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 { BlockHasher } from './blockHasher' import { Consensus, ConsensusParameters } from './consensus' import { Strategy } from './strategy' import { FISH_HASH_CONTEXT } from './testUtilities' @@ -24,12 +23,10 @@ describe('Miners reward', () => { } beforeAll(() => { - const consensus = new Consensus(consensusParameters) - const blockHasher = new BlockHasher({ consensus, context: FISH_HASH_CONTEXT }) strategy = new Strategy({ workerPool: new WorkerPool(), - consensus, - blockHasher, + consensus: new Consensus(consensusParameters), + fishHashContext: FISH_HASH_CONTEXT, }) }) diff --git a/ironfish/src/strategy.ts b/ironfish/src/strategy.ts index 31825dc22b..783c6e2073 100644 --- a/ironfish/src/strategy.ts +++ b/ironfish/src/strategy.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 { FishHashContext } from '@ironfish/rust-nodejs' import { BlockHasher } from './blockHasher' import { Consensus } from './consensus' import { Block, RawBlock } from './primitives/block' @@ -22,12 +23,15 @@ export class Strategy { constructor(options: { workerPool: WorkerPool consensus: Consensus - blockHasher: BlockHasher + fishHashContext?: FishHashContext }) { this.miningRewardCachedByYear = new Map() this.workerPool = options.workerPool this.consensus = options.consensus - this.blockHasher = options.blockHasher + this.blockHasher = new BlockHasher({ + consensus: this.consensus, + context: options.fishHashContext, + }) } /** From 4f44aae6d07c702c6664bc42adaadcbca763cf81 Mon Sep 17 00:00:00 2001 From: jowparks Date: Thu, 18 Jan 2024 16:26:44 -0800 Subject: [PATCH 21/38] chore: Ironfish rust proof generation key trait (#4565) * moves proof generation key trait to ironfish rust for better usage with external libraries * formatting * cleanup of ironfish-rust-nodejs usage of ProofGenerationKey * add license --- .../src/structs/transaction.rs | 13 ++- ironfish-rust/src/keys/mod.rs | 2 + .../src/keys}/proof_generation_key.rs | 80 ++++++++++++++----- ironfish-rust/src/lib.rs | 2 - ironfish-zkp/src/primitives/mod.rs | 2 - 5 files changed, 67 insertions(+), 32 deletions(-) rename {ironfish-zkp/src/primitives => ironfish-rust/src/keys}/proof_generation_key.rs (62%) diff --git a/ironfish-rust-nodejs/src/structs/transaction.rs b/ironfish-rust-nodejs/src/structs/transaction.rs index 010dbbd42c..9c86b4396a 100644 --- a/ironfish-rust-nodejs/src/structs/transaction.rs +++ b/ironfish-rust-nodejs/src/structs/transaction.rs @@ -6,14 +6,14 @@ use std::cell::RefCell; use std::convert::TryInto; use ironfish::assets::asset_identifier::AssetIdentifier; -use ironfish::serializing::hex_to_bytes; use ironfish::transaction::{ batch_verify_transactions, TransactionVersion, TRANSACTION_EXPIRATION_SIZE, TRANSACTION_FEE_SIZE, TRANSACTION_PUBLIC_KEY_SIZE, TRANSACTION_SIGNATURE_SIZE, }; use ironfish::{ - MerkleNoteHash, OutgoingViewKey, ProofGenerationKey, ProofGenerationKeySerializable, - ProposedTransaction, PublicAddress, SaplingKey, Transaction, ViewKey, + keys::proof_generation_key::{ProofGenerationKey, ProofGenerationKeySerializable}, + MerkleNoteHash, OutgoingViewKey, ProposedTransaction, PublicAddress, SaplingKey, Transaction, + ViewKey, }; use napi::{ bindgen_prelude::{i64n, BigInt, Buffer, Env, Object, Result, Undefined}, @@ -326,11 +326,8 @@ impl NativeTransaction { let outgoing_view_key = OutgoingViewKey::from_hex(&outgoing_view_key_str).map_err(to_napi_err)?; let public_address = PublicAddress::from_hex(&public_address_str).map_err(to_napi_err)?; - let proof_generation_key = ProofGenerationKey::deserialize( - hex_to_bytes(&proof_generation_key_str) - .map_err(|_| to_napi_err("PublicKeyPackage hex to bytes failed"))?, - ) - .map_err(to_napi_err)?; + let proof_generation_key = ProofGenerationKey::from_hex(&proof_generation_key_str) + .map_err(|_| to_napi_err("PublicKeyPackage hex to bytes failed"))?; let unsigned_transaction = self .transaction .build( diff --git a/ironfish-rust/src/keys/mod.rs b/ironfish-rust/src/keys/mod.rs index cc8e024a0c..e5b7a9a5ea 100644 --- a/ironfish-rust/src/keys/mod.rs +++ b/ironfish-rust/src/keys/mod.rs @@ -25,6 +25,8 @@ mod public_address; pub use public_address::*; mod view_keys; pub use view_keys::*; +pub mod proof_generation_key; +pub use proof_generation_key::*; #[cfg(test)] mod test; diff --git a/ironfish-zkp/src/primitives/proof_generation_key.rs b/ironfish-rust/src/keys/proof_generation_key.rs similarity index 62% rename from ironfish-zkp/src/primitives/proof_generation_key.rs rename to ironfish-rust/src/keys/proof_generation_key.rs index c376867ee3..ac19cf0119 100644 --- a/ironfish-zkp/src/primitives/proof_generation_key.rs +++ b/ironfish-rust/src/keys/proof_generation_key.rs @@ -1,11 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + use group::GroupEncoding; +pub use ironfish_zkp::ProofGenerationKey; use jubjub::{Fr, SubgroupPoint}; -use std::io::Error; -use zcash_primitives::sapling::ProofGenerationKey; + +use crate::{ + errors::{IronfishError, IronfishErrorKind}, + serializing::{bytes_to_hex, hex_to_bytes}, +}; pub trait ProofGenerationKeySerializable { fn serialize(&self) -> [u8; 64]; - fn deserialize(bytes: [u8; 64]) -> Result; + fn deserialize(bytes: [u8; 64]) -> Result; + fn hex_key(&self) -> String; + fn from_hex(hex_key: &str) -> Result; } impl ProofGenerationKeySerializable for ProofGenerationKey { @@ -16,7 +26,7 @@ impl ProofGenerationKeySerializable for ProofGenerationKey { proof_generation_key_bytes } - fn deserialize(proof_generation_key_bytes: [u8; 64]) -> Result { + fn deserialize(proof_generation_key_bytes: [u8; 64]) -> Result { let mut ak_bytes: [u8; 32] = [0; 32]; let mut nsk_bytes: [u8; 32] = [0; 32]; @@ -25,36 +35,42 @@ impl ProofGenerationKeySerializable for ProofGenerationKey { let ak = match SubgroupPoint::from_bytes(&ak_bytes).into() { Some(ak) => ak, - None => { - return Err(Error::new( - std::io::ErrorKind::InvalidData, - "Invalid proof generation key (ak)", - )) - } + None => return Err(IronfishError::new(IronfishErrorKind::InvalidAuthorizingKey)), }; let nsk = match Fr::from_bytes(&nsk_bytes).into() { Some(nsk) => nsk, None => { - return Err(Error::new( - std::io::ErrorKind::InvalidData, - "Invalid proof generation key (nsk)", + return Err(IronfishError::new( + IronfishErrorKind::InvalidNullifierDerivingKey, )) } }; Ok(ProofGenerationKey { ak, nsk }) } + + fn hex_key(&self) -> String { + let serialized_bytes = self.serialize(); + bytes_to_hex(&serialized_bytes[..]) + } + + fn from_hex(hex_key: &str) -> Result { + let bytes = hex_to_bytes(hex_key)?; + ProofGenerationKey::deserialize(bytes) + } } #[cfg(test)] mod test { - use crate::primitives::proof_generation_key::ProofGenerationKeySerializable; + use crate::errors::IronfishErrorKind; use ff::Field; use group::{Group, GroupEncoding}; + use ironfish_zkp::ProofGenerationKey; + + use super::ProofGenerationKeySerializable; use jubjub; use rand::{rngs::StdRng, SeedableRng}; - use zcash_primitives::sapling::ProofGenerationKey; // Import the rand crate for generating random bytes #[test] fn test_serialize() { @@ -81,8 +97,7 @@ mod test { let err = result.err().unwrap(); - assert_eq!(err.kind(), std::io::ErrorKind::InvalidData); - assert_eq!(err.to_string(), "Invalid proof generation key (ak)"); + assert!(matches!(err.kind, IronfishErrorKind::InvalidAuthorizingKey)); } #[test] @@ -99,9 +114,10 @@ mod test { let err = result.err().unwrap(); - assert_eq!(err.kind(), std::io::ErrorKind::InvalidData); - - assert_eq!(err.to_string(), "Invalid proof generation key (nsk)"); + assert!(matches!( + err.kind, + IronfishErrorKind::InvalidNullifierDerivingKey + )); } #[test] @@ -127,4 +143,28 @@ mod test { deserialized_proof_generation_key.nsk ); } + + #[test] + fn test_hex() { + let mut rng = StdRng::seed_from_u64(0); + + let proof_generation_key = ProofGenerationKey { + ak: jubjub::SubgroupPoint::random(&mut rng), + nsk: jubjub::Fr::random(&mut rng), + }; + + let hex_key = proof_generation_key.hex_key(); + + let deserialized_proof_generation_key = + ProofGenerationKey::from_hex(&hex_key).expect("deserialization successful"); + + assert_eq!( + proof_generation_key.ak, + deserialized_proof_generation_key.ak + ); + assert_eq!( + proof_generation_key.nsk, + deserialized_proof_generation_key.nsk + ); + } } diff --git a/ironfish-rust/src/lib.rs b/ironfish-rust/src/lib.rs index f20dfeb45b..a0b7bb8638 100644 --- a/ironfish-rust/src/lib.rs +++ b/ironfish-rust/src/lib.rs @@ -20,8 +20,6 @@ pub mod signal_catcher; pub mod transaction; pub mod util; pub mod witness; -pub use ironfish_zkp::primitives::ProofGenerationKeySerializable; -pub use ironfish_zkp::ProofGenerationKey; pub use { ironfish_frost::frost, keys::{IncomingViewKey, OutgoingViewKey, PublicAddress, SaplingKey, ViewKey}, diff --git a/ironfish-zkp/src/primitives/mod.rs b/ironfish-zkp/src/primitives/mod.rs index 3165e8abf3..1c9e55b29d 100644 --- a/ironfish-zkp/src/primitives/mod.rs +++ b/ironfish-zkp/src/primitives/mod.rs @@ -1,4 +1,2 @@ mod value_commitment; pub use value_commitment::ValueCommitment; -mod proof_generation_key; -pub use proof_generation_key::ProofGenerationKeySerializable; From 8583028644347e864980ddbbe55f9c2d3f42c8b6 Mon Sep 17 00:00:00 2001 From: jowparks Date: Thu, 18 Jan 2024 16:49:32 -0800 Subject: [PATCH 22/38] creates method for taking commitments from participants and creating a signing package (#4562) --- .../src/structs/transaction.rs | 2 +- ironfish-rust/src/transaction/unsigned.rs | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/ironfish-rust-nodejs/src/structs/transaction.rs b/ironfish-rust-nodejs/src/structs/transaction.rs index 9c86b4396a..223655aeb0 100644 --- a/ironfish-rust-nodejs/src/structs/transaction.rs +++ b/ironfish-rust-nodejs/src/structs/transaction.rs @@ -3,6 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::cell::RefCell; + use std::convert::TryInto; use ironfish::assets::asset_identifier::AssetIdentifier; @@ -174,7 +175,6 @@ impl NativeTransaction { pub fn new(version: u8) -> Result { let tx_version = version.try_into().map_err(to_napi_err)?; let transaction = ProposedTransaction::new(tx_version); - Ok(NativeTransaction { transaction }) } diff --git a/ironfish-rust/src/transaction/unsigned.rs b/ironfish-rust/src/transaction/unsigned.rs index fc2a74005d..0644fe873f 100644 --- a/ironfish-rust/src/transaction/unsigned.rs +++ b/ironfish-rust/src/transaction/unsigned.rs @@ -4,8 +4,13 @@ use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use group::GroupEncoding; +use ironfish_frost::frost::{round1::SigningCommitments, Identifier, SigningPackage}; + use ironfish_zkp::redjubjub::{self, Signature}; -use std::io::{self, Write}; +use std::{ + collections::BTreeMap, + io::{self, Write}, +}; use crate::{ errors::IronfishError, serializing::read_scalar, transaction::Blake2b, OutputDescription, @@ -209,4 +214,15 @@ impl UnsignedTransaction { randomized_public_key: self.randomized_public_key.clone(), }) } + + // Creates frost signing package for use in round two of FROST multisig protocol + // only applicable for multisig transactions + pub fn signing_package( + &self, + commitments: BTreeMap, + ) -> Result { + // Create the transaction signature hash + let data_to_sign = self.transaction_signature_hash()?; + Ok(SigningPackage::new(commitments, &data_to_sign)) + } } From b879e8b796e04c3103fa25a51e7d72621bb6a654 Mon Sep 17 00:00:00 2001 From: jowparks Date: Fri, 19 Jan 2024 09:53:23 -0800 Subject: [PATCH 23/38] outputs hex string for proof generation key in napi bindings (#4564) --- ironfish-rust-nodejs/index.d.ts | 9 +++++++++ ironfish-rust-nodejs/index.js | 3 ++- ironfish-rust-nodejs/src/lib.rs | 5 +++++ ironfish-rust/src/keys/mod.rs | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index 05b62ceb3d..acb5fc40e8 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -3,6 +3,13 @@ /* auto-generated by NAPI-RS */ +export interface RoundOneSigningData { + nonceHiding: string + nonceBinding: string + commitmentHiding: string + commitmentBinding: string +} +export function roundOne(keyPackage: string, seed: number): RoundOneSigningData 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 @@ -75,6 +82,7 @@ export interface Key { incomingViewKey: string outgoingViewKey: string publicAddress: string + proofGenerationKey: string } export function generateKey(): Key export function spendingKeyToWords(privateKey: string, languageCode: LanguageCode): string @@ -215,6 +223,7 @@ export class Transaction { * aka: self.value_balance - intended_transaction_fee - change = 0 */ post(spenderHexKey: string, changeGoesTo: string | undefined | null, intendedTransactionFee: bigint): Buffer + build(proofGenerationKeyStr: string, viewKeyStr: string, outgoingViewKeyStr: string, publicAddressStr: string, intendedTransactionFee: bigint): Buffer setExpiration(sequence: number): void } export class FoundBlockResult { diff --git a/ironfish-rust-nodejs/index.js b/ironfish-rust-nodejs/index.js index 5b2524eb0b..df7b6811f1 100644 --- a/ironfish-rust-nodejs/index.js +++ b/ironfish-rust-nodejs/index.js @@ -252,9 +252,10 @@ 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, LanguageCode, generateKey, spendingKeyToWords, wordsToSpendingKey, generateKeyFromPrivateKey, initializeSapling, FoundBlockResult, ThreadPoolHandler, isValidPublicAddress } = nativeBinding +const { FishHashContext, roundOne, 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, LanguageCode, generateKey, spendingKeyToWords, wordsToSpendingKey, generateKeyFromPrivateKey, initializeSapling, FoundBlockResult, ThreadPoolHandler, isValidPublicAddress } = nativeBinding module.exports.FishHashContext = FishHashContext +module.exports.roundOne = roundOne module.exports.contribute = contribute module.exports.verifyTransform = verifyTransform module.exports.KEY_LENGTH = KEY_LENGTH diff --git a/ironfish-rust-nodejs/src/lib.rs b/ironfish-rust-nodejs/src/lib.rs index 57e481069b..6109d5c1ad 100644 --- a/ironfish-rust-nodejs/src/lib.rs +++ b/ironfish-rust-nodejs/src/lib.rs @@ -5,8 +5,10 @@ use std::fmt::Display; use ironfish::keys::Language; +use ironfish::keys::ProofGenerationKeySerializable; use ironfish::PublicAddress; use ironfish::SaplingKey; + use napi::bindgen_prelude::*; use napi_derive::napi; @@ -63,6 +65,7 @@ pub struct Key { pub incoming_view_key: String, pub outgoing_view_key: String, pub public_address: String, + pub proof_generation_key: String, } #[napi] @@ -75,6 +78,7 @@ pub fn generate_key() -> Key { incoming_view_key: sapling_key.incoming_view_key().hex_key(), outgoing_view_key: sapling_key.outgoing_view_key().hex_key(), public_address: sapling_key.public_address().hex_public_address(), + proof_generation_key: sapling_key.sapling_proof_generation_key().hex_key(), } } @@ -101,6 +105,7 @@ pub fn generate_key_from_private_key(private_key: String) -> Result { incoming_view_key: sapling_key.incoming_view_key().hex_key(), outgoing_view_key: sapling_key.outgoing_view_key().hex_key(), public_address: sapling_key.public_address().hex_public_address(), + proof_generation_key: sapling_key.sapling_proof_generation_key().hex_key(), }) } diff --git a/ironfish-rust/src/keys/mod.rs b/ironfish-rust/src/keys/mod.rs index e5b7a9a5ea..97cbf98535 100644 --- a/ironfish-rust/src/keys/mod.rs +++ b/ironfish-rust/src/keys/mod.rs @@ -208,7 +208,7 @@ impl SaplingKey { /// Adapter to convert this key to a proof generation key for use in /// sapling functions - pub(crate) fn sapling_proof_generation_key(&self) -> ProofGenerationKey { + pub fn sapling_proof_generation_key(&self) -> ProofGenerationKey { ProofGenerationKey { ak: self.view_key.authorizing_key, nsk: self.proof_authorizing_key, From aefb818cf910a037579bad48e54b288ec1999100 Mon Sep 17 00:00:00 2001 From: lovedret Date: Sat, 20 Jan 2024 05:50:07 +0800 Subject: [PATCH 24/38] Use createdAt as head of imported account (#4549) * Use createdAt as head of imported account * Fix test about importAccount * sets head of imported account to createdAt - 1 the head of an account refers to the latest block in the chain that the account has scanned createdAt refers to the head of the chain when an account was created or to the earliest block in the chain on which an account received a transaction when importing an account we can set the account head to the block previous to 'createdAt'. we cannot set it to createdAt because the account may have received tokens in that block (e.g., the genesis block) * removes extra block lookup * fixes lint --------- Co-authored-by: Hugh Cunningham --- .../__fixtures__/wallet.test.ts.fixture | 82 +++++++++++++++++++ ironfish/src/wallet/wallet.test.ts | 35 +++++++- ironfish/src/wallet/wallet.ts | 34 +++++++- 3 files changed, 148 insertions(+), 3 deletions(-) diff --git a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture index d4e671eebd..5222278ef3 100644 --- a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture +++ b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture @@ -6578,5 +6578,87 @@ } ] } + ], + "Wallet importAccount should set account head to block before createdAt if that block is in the chain": [ + { + "version": 2, + "id": "19105b44-3480-4859-a7f3-f09256bb44e5", + "name": "accountA", + "spendingKey": "05cba3e7a7f92918fc22ef8f4eec46108240d765a315666e948fed8b772657f2", + "viewKey": "8d77dfd819889baefdbab586c7ea2615048485f425b688c02c901ccd29caa940cc4b57abc0f4fa659d2dad8e9422fa3d7bb5a98202001f04dc68df99b08b31e1", + "incomingViewKey": "29f2723c30f767ffb71275561c9157f24a66147e0d9026aed291d5a1731e4e01", + "outgoingViewKey": "2562410792eb1b13ba3d23af607edbccc62dee57d43446ba2ce4fb6768661649", + "publicAddress": "1b6a521ae70b9347cba2762564f668350072fcfdec3551c609931a7a04821716", + "createdAt": null + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86", + "noteCommitment": { + "type": "Buffer", + "data": "base64:X2rrDF8DvvDjuYkL3RP00HmSOV5QjCPoj6q3GTfiyy0=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:I77dMcBFEcVW/EmtnPGG2LcgcX4Sy04//z6lFTj91Gk=" + }, + "target": "9282972777491357380673661573939192202192629606981189395159182914949423", + "randomness": "0", + "timestamp": 1705617605324, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AgAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAX2JrAqJc52Lnj69hIqAwyrAa3qEyIeRcI9BljFONqXKqpqyWnG4z1LW3MGtwCeAGzyvuDa0kE+jfuNtEotEdWmH+SHl6lXTxTRrqhfEx6NSIx2sEA78fUwyvffgPZjPNqjUtYLv02bBIIZlzXO9qB+ck20DyuIRWteeR3S/zNjcMx/yi+F3PlBe8Lw2EC8Ow7+LSiyiUPxDYZcxK/L5b8l+4Xn32KRl6zM4m/uanqzeGIu7HTCDHrA/k7BshEvSMHzR+R8+C3/boCnSW4piaT+2yPCWnuskHsNmZLUiY6brBKWPxamIzGlD6CqG3ibTVAmKmpaBlBVVoNme+oEFM4Nql2OaveEIzwxOfH8py9/3D1+IjVmyqFw5nOZA3WdJWmADB6aZQsqbab0KoPFFpyGLzaKy40tuBBKdRnvuF0pGYC3AjLwyGo/q97cdlLXPrO2SE4K3UgeAUCJ4LhXC7Kf4lGO4uLYkJUVdT1IPrq5LH4LiPrw2uioSg140plPhUqJ5OttLJ73CGwtrkNNVcaCaMe9IC9MuM7egzSdFah8R1w8KCrstkypBHk1zsJzUKR7rvm+EXWX2ykxRKDFHAb+W5rFuufky8FM1ijff1F3tq6CV6LgEQ6klyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBnCKNSha+w6GxP7ztj4utzjjPYUOXwG5k/wJ5uEUb2lgajonWHEmSo0bu7+KdRM5Q73cWqqTisFy/Px7lUSWCA==" + } + ] + }, + { + "header": { + "sequence": 3, + "previousBlockHash": "C65E41C99C4F94AB6DA5C7108656CAFA0E1CCAD30BEBA935B6E716AD79AF62AD", + "noteCommitment": { + "type": "Buffer", + "data": "base64:7Jy4spBTZZSA7Yx55FuaBmHw4MdtyZLdKsq8ip33Nz8=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:VCc34674Y7XSfKgj1sdyPPom6aXVeTw+p6H+BQD7qpU=" + }, + "target": "9255858786337818395603165512831024101510453493377417362192396248796027", + "randomness": "0", + "timestamp": 1705617605785, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 5, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AgAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAVKamsZytTRJX6nItjqAvpsG7PXcufN3pVlMQSE0CgYyPSEN9jwtlcSuO41fjzggn3dITCwiYhSl8ZEjUyoJ7aEwyM+LfxU7HKHEYnscpUueJnWQI65U2Imf6StoJCdl1mul9xQ9B4c+Mkf8JHHGePTROHEiVdUNRrCrrbAPU6RcZKQq9JzwnGkYvHEJ09e8OhtaE5rg0OqD1Kc/zNIGYHPfuqCHykhOFZI8OCPkOqjSR4VGskqHvYNoCzfjiVt09TCA3XCQ4uiuN2POGw3fv6W9YvB9DQTiE1g93Hb2rS9mW7KXNEIF2jgaTb6erWlxExFkW19Uo6+biF+dMHJ/c4dunGkEQdinhLBu7/u5Kvec+T7oPpH+LPAbd554Vt8kMNUy11yzLOpbtk2+tJiLNbT4sEYL79LHigxHNqszmWEhFNlIXsrdJ3EzYYjajOxa/1RnlA4rNeIfSCx6pz91ydblcF8LiKrJELOWsjzDThXY6D4oVp2V3ZWvWx20Ilb8LMYToq1dQX0GQta6Ff5mi+GESYluGrVDYZ3ci201E+uSRiucJJoJMxu5Gbh3h1kPcnyke9q65Es+6mRV4E3mMbfU1mFcNiAEDWGT9EMQmRy+NKRAYKJRaYUlyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwn3vn3NRdAhBEFv6wBrS++9bXls8KL2iPx+au4KmVuZSfAc+JpnO2qL+AfbRhSJcbXAESLw6Q2rgeVlGY8MWJBQ==" + } + ] + }, + { + "version": 2, + "id": "1135573e-0006-48e1-a75b-69570c72f465", + "name": "accountB", + "spendingKey": "4f3b4874e45ed10f6fce62a82c2b8abf6c8ed9a9acd6d80750cc2447e83b1aff", + "viewKey": "e49cfa6090a055afc02709351a4ae5b183644f3a78b2b3d56e1ce778a64bc8bb5f714b6d87924817ad44f47a15fb6c0c4a49f226e04af499e20df43dc4b68eeb", + "incomingViewKey": "5b8b4fad24206f01b4fc06bb9b1ba65af7a4bd0d2832f390cb8767b9c0b69906", + "outgoingViewKey": "e053e198b3a9d7fbf40e965751b14587429ab86aac0dbd2eef79bc684b706890", + "publicAddress": "c8ff1a02370a52a7c4e51217553a34ac4faa0d252942645f399ab5f93f810fee", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:Yn/PyqoXeLhkW7xQ7kGrHxMOtm93Emv/To3SQv/MqGU=" + }, + "sequence": 3 + } + } ] } \ No newline at end of file diff --git a/ironfish/src/wallet/wallet.test.ts b/ironfish/src/wallet/wallet.test.ts index a63b654df1..8420e35bbe 100644 --- a/ironfish/src/wallet/wallet.test.ts +++ b/ironfish/src/wallet/wallet.test.ts @@ -692,6 +692,39 @@ describe('Wallet', () => { expect(accountBImport.createdAt?.sequence).toEqual(3) }) + it('should set account head to block before createdAt if that block is in the chain', async () => { + const { node: nodeA } = await nodeTest.createSetup() + const { node: nodeB } = await nodeTest.createSetup() + + const accountA = await useAccountFixture(nodeA.wallet, 'accountA', { + setCreatedAt: false, + }) + expect(accountA.createdAt).toBe(null) + + // create blocks and add them to both chains + const block2 = await useMinerBlockFixture(nodeA.chain, 2) + await nodeA.chain.addBlock(block2) + await nodeB.chain.addBlock(block2) + await nodeA.wallet.updateHead() + const block3 = await useMinerBlockFixture(nodeA.chain, 3) + await nodeA.chain.addBlock(block3) + await nodeB.chain.addBlock(block3) + await nodeA.wallet.updateHead() + + // create an account so that createdAt will be non-null + const accountB = await useAccountFixture(nodeA.wallet, 'accountB') + + const accountBImport = await nodeB.wallet.importAccount(accountB) + + expect(accountBImport.createdAt?.hash).toEqualHash(block3.header.hash) + expect(accountBImport.createdAt?.sequence).toEqual(3) + + const accountBImportHead = await accountBImport.getHead() + + expect(accountBImportHead?.hash).toEqualHash(block2.header.hash) + expect(accountBImportHead?.sequence).toEqual(2) + }) + it('should set createdAt to null if that block is not in the chain', async () => { const { node: nodeA } = await nodeTest.createSetup() const { node: nodeB } = await nodeTest.createSetup() @@ -717,7 +750,7 @@ describe('Wallet', () => { const accountBImport = await nodeB.wallet.importAccount(accountB) - expect(accountBImport.createdAt).toBeNull() + expect(accountBImport.createdAt).toBeDefined() }) }) diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index 8eed7f5f3d..f3e8a75afe 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -14,7 +14,7 @@ import { NoteHasher } from '../merkletree' import { Side } from '../merkletree/merkletree' import { Witness } from '../merkletree/witness' import { Mutex } from '../mutex' -import { GENESIS_BLOCK_SEQUENCE } from '../primitives' +import { GENESIS_BLOCK_PREVIOUS, GENESIS_BLOCK_SEQUENCE } from '../primitives/block' import { BurnDescription } from '../primitives/burnDescription' import { MintDescription } from '../primitives/mintDescription' import { Note } from '../primitives/note' @@ -1544,7 +1544,21 @@ export class Wallet { await this.walletDb.db.transaction(async (tx) => { await this.walletDb.setAccount(account, tx) - await account.updateHead(null, tx) + + if (createdAt !== null) { + const previousBlockHash = await this.chainGetPreviousBlockHash(createdAt.hash) + + const head = previousBlockHash + ? { + hash: previousBlockHash, + sequence: createdAt.sequence - 1, + } + : null + + await account.updateHead(head, tx) + } else { + await account.updateHead(null, tx) + } }) this.accounts.set(account.id, account) @@ -1767,6 +1781,22 @@ export class Wallet { } } + async chainGetPreviousBlockHash(hash: Buffer): Promise { + const block = await this.chainGetBlock({ hash: hash.toString('hex') }) + + if (block === null) { + return null + } + + const previousBlockHash = Buffer.from(block.block.previousBlockHash, 'hex') + + if (previousBlockHash.equals(GENESIS_BLOCK_PREVIOUS)) { + return null + } + + return previousBlockHash + } + private async getChainAsset(id: Buffer): Promise<{ createdTransactionHash: Buffer creator: Buffer From 07ca7b89f909996e53bc2fcb4fffae663955631d Mon Sep 17 00:00:00 2001 From: Daniel Cogan Date: Fri, 19 Jan 2024 14:21:01 -0800 Subject: [PATCH 25/38] Refactor isActive function on Consensus (#4567) --- ironfish/src/blockHasher.ts | 5 +- ironfish/src/consensus/consensus.test.ts | 111 ++++++++++++++--------- ironfish/src/consensus/consensus.ts | 9 +- ironfish/src/consensus/verifier.ts | 7 +- 4 files changed, 75 insertions(+), 57 deletions(-) diff --git a/ironfish/src/blockHasher.ts b/ironfish/src/blockHasher.ts index 9814a79d5a..fb0b5008ab 100644 --- a/ironfish/src/blockHasher.ts +++ b/ironfish/src/blockHasher.ts @@ -24,10 +24,7 @@ export class BlockHasher { } hashHeader(header: RawBlockHeader): BlockHash { - const useFishHash = this.consensus.isActive( - this.consensus.parameters.enableFishHash, - header.sequence, - ) + const useFishHash = this.consensus.isActive('enableFishHash', header.sequence) if (useFishHash) { Assert.isNotNull(this.fishHashContext, 'FishHash context was not initialized') diff --git a/ironfish/src/consensus/consensus.test.ts b/ironfish/src/consensus/consensus.test.ts index 46fcc956f5..5380d53e94 100644 --- a/ironfish/src/consensus/consensus.test.ts +++ b/ironfish/src/consensus/consensus.test.ts @@ -14,65 +14,90 @@ describe('Consensus', () => { maxBlockSizeBytes: 5, minFee: 6, enableAssetOwnership: 7, - enforceSequentialBlockTime: 1, - enableFishHash: 'never', + enforceSequentialBlockTime: 8, + enableFishHash: 9, } - let consensus: Consensus + const consensus = new Consensus(params) - beforeAll(() => { - consensus = new Consensus(params) + const consensusWithNevers = new Consensus({ + ...params, + enableAssetOwnership: 'never', + enforceSequentialBlockTime: 'never', + enableFishHash: 'never', }) describe('isActive', () => { - describe('returns false when the sequence is less than the upgrade number', () => { - const upgradeSequence = 5 - for (let sequence = 1; sequence < upgradeSequence; sequence++) { - it(`sequence: ${sequence}`, () => { - expect(consensus.isActive(upgradeSequence, sequence)).toBe(false) - }) - } + it('returns false when the sequence is less than the upgrade number', () => { + expect(consensus.isActive('genesisSupplyInIron', 1)).toBe(false) + expect(consensus.isActive('targetBlockTimeInSeconds', 2)).toBe(false) + expect(consensus.isActive('targetBucketTimeInSeconds', 3)).toBe(false) + expect(consensus.isActive('maxBlockSizeBytes', 4)).toBe(false) + expect(consensus.isActive('minFee', 5)).toBe(false) + expect(consensus.isActive('enableAssetOwnership', 6)).toBe(false) + expect(consensus.isActive('enforceSequentialBlockTime', 7)).toBe(false) + expect(consensus.isActive('enableFishHash', 8)).toBe(false) + }) + + it('returns true when the sequence is equal to the upgrade number', () => { + expect(consensus.isActive('genesisSupplyInIron', 2)).toBe(true) + expect(consensus.isActive('targetBlockTimeInSeconds', 3)).toBe(true) + expect(consensus.isActive('targetBucketTimeInSeconds', 4)).toBe(true) + expect(consensus.isActive('maxBlockSizeBytes', 5)).toBe(true) + expect(consensus.isActive('minFee', 6)).toBe(true) + expect(consensus.isActive('enableAssetOwnership', 7)).toBe(true) + expect(consensus.isActive('enforceSequentialBlockTime', 8)).toBe(true) + expect(consensus.isActive('enableFishHash', 9)).toBe(true) }) - describe('returns true when the sequence is greater than or equal to the upgrade number', () => { - const upgradeSequence = 5 - for (let sequence = upgradeSequence; sequence < upgradeSequence * 2; sequence++) { - it(`sequence: ${sequence}`, () => { - expect(consensus.isActive(upgradeSequence, sequence)).toBe(true) - }) - } + it('returns true when the sequence is greater than the upgrade number', () => { + expect(consensus.isActive('genesisSupplyInIron', 3)).toBe(true) + expect(consensus.isActive('targetBlockTimeInSeconds', 4)).toBe(true) + expect(consensus.isActive('targetBucketTimeInSeconds', 5)).toBe(true) + expect(consensus.isActive('maxBlockSizeBytes', 6)).toBe(true) + expect(consensus.isActive('minFee', 7)).toBe(true) + expect(consensus.isActive('enableAssetOwnership', 8)).toBe(true) + expect(consensus.isActive('enforceSequentialBlockTime', 9)).toBe(true) + expect(consensus.isActive('enableFishHash', 10)).toBe(true) }) it('uses a minimum sequence of 1 if given a smaller sequence', () => { - const upgradeSequence = 1 - expect(consensus.isActive(upgradeSequence, -100)).toBe(true) - expect(consensus.isActive(upgradeSequence, -1)).toBe(true) - expect(consensus.isActive(upgradeSequence, 0)).toBe(true) + expect(consensus.isActive('allowedBlockFutureSeconds', -100)).toBe(true) + expect(consensus.isActive('allowedBlockFutureSeconds', -1)).toBe(true) + expect(consensus.isActive('allowedBlockFutureSeconds', 0)).toBe(true) + }) + + it('returns false if flag activation is never', () => { + expect(consensusWithNevers.isActive('enableAssetOwnership', 3)).toBe(false) + expect(consensusWithNevers.isActive('enforceSequentialBlockTime', 3)).toBe(false) + expect(consensusWithNevers.isActive('enableFishHash', 3)).toBe(false) }) }) - it('getActiveTransactionVersion', () => { - expect(consensus.getActiveTransactionVersion(5)).toEqual(TransactionVersion.V1) - expect(consensus.getActiveTransactionVersion(6)).toEqual(TransactionVersion.V1) - expect(consensus.getActiveTransactionVersion(7)).toEqual(TransactionVersion.V2) - expect(consensus.getActiveTransactionVersion(8)).toEqual(TransactionVersion.V2) + describe('isNeverActive', () => { + it('returns true if flag activation is never', () => { + expect(consensusWithNevers.isNeverActive('enableAssetOwnership')).toBe(true) + expect(consensusWithNevers.isNeverActive('enforceSequentialBlockTime')).toBe(true) + expect(consensusWithNevers.isNeverActive('enableFishHash')).toBe(true) + }) + + it('returns false if flag has activation sequence', () => { + expect(consensus.isNeverActive('enableAssetOwnership')).toBe(false) + expect(consensus.isNeverActive('enforceSequentialBlockTime')).toBe(false) + expect(consensus.isNeverActive('enableFishHash')).toBe(false) + }) }) - it('when activation flag is never', () => { - consensus = new Consensus({ - allowedBlockFutureSeconds: 1, - genesisSupplyInIron: 2, - targetBlockTimeInSeconds: 3, - targetBucketTimeInSeconds: 4, - maxBlockSizeBytes: 5, - minFee: 6, - enableAssetOwnership: 'never', - enforceSequentialBlockTime: 'never', - enableFishHash: 'never', + describe('getActiveTransactionVersion', () => { + it('returns the correct transaction version based on activation sequence', () => { + expect(consensus.getActiveTransactionVersion(5)).toEqual(TransactionVersion.V1) + expect(consensus.getActiveTransactionVersion(6)).toEqual(TransactionVersion.V1) + expect(consensus.getActiveTransactionVersion(7)).toEqual(TransactionVersion.V2) + expect(consensus.getActiveTransactionVersion(8)).toEqual(TransactionVersion.V2) + }) + + it('returns V1 transaction when activation flag is never', () => { + expect(consensusWithNevers.getActiveTransactionVersion(5)).toEqual(TransactionVersion.V1) }) - expect(consensus.getActiveTransactionVersion(5)).toEqual(TransactionVersion.V1) - expect(consensus.isActive(consensus.parameters.enableAssetOwnership, 3)).toBe(false) - expect(consensus.isActive(consensus.parameters.enforceSequentialBlockTime, 3)).toBe(false) - expect(consensus.isActive(consensus.parameters.enableFishHash, 3)).toBe(false) }) }) diff --git a/ironfish/src/consensus/consensus.ts b/ironfish/src/consensus/consensus.ts index 8d20e9206d..569b918b19 100644 --- a/ironfish/src/consensus/consensus.ts +++ b/ironfish/src/consensus/consensus.ts @@ -64,11 +64,12 @@ export class Consensus { this.parameters = parameters } - isActive(upgrade: ActivationSequence, sequence: number): boolean { - if (upgrade === 'never') { + isActive(upgrade: keyof ConsensusParameters, sequence: number): boolean { + const upgradeSequence = this.parameters[upgrade] + if (upgradeSequence === 'never') { return false } - return Math.max(1, sequence) >= upgrade + return Math.max(1, sequence) >= upgradeSequence } /** @@ -79,7 +80,7 @@ export class Consensus { } getActiveTransactionVersion(sequence: number): TransactionVersion { - if (this.isActive(this.parameters.enableAssetOwnership, sequence)) { + if (this.isActive('enableAssetOwnership', sequence)) { return TransactionVersion.V2 } else { return TransactionVersion.V1 diff --git a/ironfish/src/consensus/verifier.ts b/ironfish/src/consensus/verifier.ts index 1d01835b27..67d80c2765 100644 --- a/ironfish/src/consensus/verifier.ts +++ b/ironfish/src/consensus/verifier.ts @@ -217,12 +217,7 @@ export class Verifier { return { valid: false, reason: VerificationResultReason.PREV_HASH_MISMATCH } } - if ( - this.chain.consensus.isActive( - this.chain.consensus.parameters.enforceSequentialBlockTime, - current.sequence, - ) - ) { + if (this.chain.consensus.isActive('enforceSequentialBlockTime', current.sequence)) { if (current.timestamp.getTime() <= previousHeader.timestamp.getTime()) { return { valid: false, reason: VerificationResultReason.BLOCK_TOO_OLD } } From 8e718b211024c3df2c4258792396315f7f47011c Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Fri, 19 Jan 2024 16:05:03 -0800 Subject: [PATCH 26/38] Renamed many RPC types for consistency (#4573) All RPC types now have an "Rpc" prefix. This is useful so you don't mix Rpc types unknowingly in other systems. Names now generally follow this protocol 1. Rpc 2. Adapter (ex Socket, Ipc, Http) (Optional if applies to all protocols) 3. Server | Client (Optional if applies to both) 4. Thing (Ex Response, Error, etc) 5. Schema (Optional if this is a schema for that thing) --- ironfish/src/rpc/adapters/errors.ts | 6 +-- ironfish/src/rpc/adapters/httpAdapter.ts | 16 +++---- ironfish/src/rpc/adapters/ipcAdapter.test.ts | 4 +- ironfish/src/rpc/adapters/memoryAdapter.ts | 4 +- .../rpc/adapters/socketAdapter/protocol.ts | 30 ++++++------ .../adapters/socketAdapter/socketAdapter.ts | 46 ++++++++++--------- ironfish/src/rpc/adapters/tcpAdapter.test.ts | 4 +- ironfish/src/rpc/clients/errors.ts | 2 +- ironfish/src/rpc/clients/socketClient.ts | 24 +++++----- .../rpc/routes/chain/broadcastTransaction.ts | 4 +- ironfish/src/rpc/routes/chain/getAsset.ts | 6 +-- ironfish/src/rpc/routes/chain/getBlock.ts | 8 ++-- .../src/rpc/routes/chain/getDifficulty.ts | 4 +- .../rpc/routes/chain/getNetworkHashPower.ts | 4 +- .../src/rpc/routes/chain/getNoteWitness.ts | 4 +- .../src/rpc/routes/chain/getTransaction.ts | 10 ++-- .../rpc/routes/chain/getTransactionStream.ts | 8 ++-- ironfish/src/rpc/routes/config/getConfig.ts | 4 +- .../src/rpc/routes/config/uploadConfig.ts | 8 ++-- ironfish/src/rpc/routes/faucet/getFunds.ts | 15 +++--- ironfish/src/rpc/routes/router.ts | 10 ++-- .../src/rpc/routes/wallet/addTransaction.ts | 6 +-- ironfish/src/rpc/routes/wallet/create.ts | 4 +- .../src/rpc/routes/wallet/createAccount.ts | 4 +- .../rpc/routes/wallet/createTransaction.ts | 8 ++-- ironfish/src/rpc/routes/wallet/getAsset.ts | 6 +-- .../src/rpc/routes/wallet/getNodeStatus.ts | 4 +- ironfish/src/rpc/routes/wallet/mintAsset.ts | 4 +- .../src/rpc/routes/wallet/rescanAccount.ts | 6 +-- .../src/rpc/routes/wallet/sendTransaction.ts | 8 ++-- ironfish/src/rpc/routes/wallet/utils.ts | 6 +-- 31 files changed, 142 insertions(+), 135 deletions(-) diff --git a/ironfish/src/rpc/adapters/errors.ts b/ironfish/src/rpc/adapters/errors.ts index fbbc2c45e9..39c8e83ff1 100644 --- a/ironfish/src/rpc/adapters/errors.ts +++ b/ironfish/src/rpc/adapters/errors.ts @@ -22,7 +22,7 @@ export enum ERROR_CODES { * * @note Look at the {@link IPCAdapter} implementation for an example */ -export class ResponseError extends Error { +export class RpcResponseError extends Error { name = this.constructor.name status: number code: string @@ -47,7 +47,7 @@ export class ResponseError extends Error { * A convenience error to throw inside of routes when you want to indicate * a 400 error to the user based on validation */ -export class ValidationError extends ResponseError { +export class RpcValidationError extends RpcResponseError { constructor(message: string, status = 400, code = ERROR_CODES.VALIDATION) { super(message, code, status) } @@ -56,7 +56,7 @@ export class ValidationError extends ResponseError { /** * A convenience error to throw inside of routes when a resource is not found */ -export class NotFoundError extends ResponseError { +export class RpcNotFoundError extends RpcResponseError { constructor(message: string, status = 404, code = ERROR_CODES.NOT_FOUND) { super(message, code, status) } diff --git a/ironfish/src/rpc/adapters/httpAdapter.ts b/ironfish/src/rpc/adapters/httpAdapter.ts index 95268097dd..e96aed094f 100644 --- a/ironfish/src/rpc/adapters/httpAdapter.ts +++ b/ironfish/src/rpc/adapters/httpAdapter.ts @@ -11,13 +11,13 @@ import { RpcRequest } from '../request' import { ApiNamespace, Router } from '../routes' import { RpcServer } from '../server' import { IRpcAdapter } from './adapter' -import { ERROR_CODES, ResponseError } from './errors' +import { ERROR_CODES, RpcResponseError } from './errors' import { MESSAGE_DELIMITER } from './socketAdapter' const MEGABYTES = 1000 * 1000 const MAX_REQUEST_SIZE = 5 * MEGABYTES -export type HttpRpcError = { +export type RpcHttpError = { status: number code: string message: string @@ -101,13 +101,13 @@ export class RpcHttpAdapter implements IRpcAdapter { void this.handleRequest(req, res, requestId).catch((e) => { const error = ErrorUtils.renderError(e) this.logger.debug(`Error in HTTP adapter: ${error}`) - let errorResponse: HttpRpcError = { + let errorResponse: RpcHttpError = { code: ERROR_CODES.ERROR, status: 500, message: error, } - if (e instanceof ResponseError) { + if (e instanceof RpcResponseError) { errorResponse = { code: e.code, status: e.status, @@ -165,13 +165,13 @@ export class RpcHttpAdapter implements IRpcAdapter { requestId: string, ): Promise { if (this.router === null || this.router.server === null) { - throw new ResponseError('Tried to connect to unmounted adapter') + throw new RpcResponseError('Tried to connect to unmounted adapter') } const router = this.router if (request.url === undefined) { - throw new ResponseError('No request url provided') + throw new RpcResponseError('No request url provided') } this.logger.debug( @@ -180,7 +180,7 @@ export class RpcHttpAdapter implements IRpcAdapter { const route = this.formatRoute(request) if (route === undefined) { - throw new ResponseError('No route found') + throw new RpcResponseError('No route found') } // TODO(daniel): clean up reading body code here a bit of possible @@ -193,7 +193,7 @@ export class RpcHttpAdapter implements IRpcAdapter { data.push(chunk) if (size >= MAX_REQUEST_SIZE) { - throw new ResponseError('Max request size exceeded') + throw new RpcResponseError('Max request size exceeded') } } diff --git a/ironfish/src/rpc/adapters/ipcAdapter.test.ts b/ironfish/src/rpc/adapters/ipcAdapter.test.ts index ea65ef3386..4c4bfaa058 100644 --- a/ironfish/src/rpc/adapters/ipcAdapter.test.ts +++ b/ironfish/src/rpc/adapters/ipcAdapter.test.ts @@ -9,7 +9,7 @@ import { PromiseUtils } from '../../utils/promise' import { RpcRequestError } from '../clients' import { RpcIpcClient } from '../clients/ipcClient' import { ALL_API_NAMESPACES } from '../routes/router' -import { ERROR_CODES, ValidationError } from './errors' +import { ERROR_CODES, RpcValidationError } from './errors' import { RpcIpcAdapter } from './ipcAdapter' describe('IpcAdapter', () => { @@ -102,7 +102,7 @@ describe('IpcAdapter', () => { it('should handle errors', async () => { ipc.router?.routes.register('foo/bar', yup.object({}), () => { - throw new ValidationError('hello error', 402, 'hello-error' as ERROR_CODES) + throw new RpcValidationError('hello error', 402, 'hello-error' as ERROR_CODES) }) await ipc.start() diff --git a/ironfish/src/rpc/adapters/memoryAdapter.ts b/ironfish/src/rpc/adapters/memoryAdapter.ts index 0f91d0ba75..2d0664268a 100644 --- a/ironfish/src/rpc/adapters/memoryAdapter.ts +++ b/ironfish/src/rpc/adapters/memoryAdapter.ts @@ -8,7 +8,7 @@ import { RpcRequest } from '../request' import { RpcResponse } from '../response' import { Router } from '../routes' import { Stream } from '../stream' -import { ResponseError } from './errors' +import { RpcResponseError } from './errors' /** * This class provides a way to route requests directly against the routing layer @@ -48,7 +48,7 @@ export class RpcMemoryAdapter { response.routePromise = router.route(route, request).catch((e) => { stream.close() - if (e instanceof ResponseError) { + if (e instanceof RpcResponseError) { // Set the response status to the errors status because RequsetError takes it from the response response.status = e.status diff --git a/ironfish/src/rpc/adapters/socketAdapter/protocol.ts b/ironfish/src/rpc/adapters/socketAdapter/protocol.ts index 5335facb84..95a38cccd2 100644 --- a/ironfish/src/rpc/adapters/socketAdapter/protocol.ts +++ b/ironfish/src/rpc/adapters/socketAdapter/protocol.ts @@ -5,41 +5,41 @@ import * as yup from 'yup' export const MESSAGE_DELIMITER = '\f' -export type ClientSocketRpc = { +export type RpcSocketClientMessage = { type: 'message' - data: SocketRpcRequest + data: RpcSocketRequest } -export type ServerSocketRpc = { +export type RpcSocketServerMessage = { type: 'message' | 'malformedRequest' | 'error' | 'stream' - data: SocketRpcResponse | SocketRpcError | SocketRpcStream + data: RpcSocketResponse | RpcSocketError | RpcSocketStream } -export type SocketRpcRequest = { +export type RpcSocketRequest = { mid: number type: string auth: string | null | undefined data: unknown } -export type SocketRpcResponse = { +export type RpcSocketResponse = { id: number status: number data: unknown } -export type SocketRpcStream = { +export type RpcSocketStream = { id: number data: unknown } -export type SocketRpcError = { +export type RpcSocketError = { code: string message: string stack?: string } -export const ClientSocketRpcSchema: yup.ObjectSchema = yup +export const RpcSocketClientMessageSchema: yup.ObjectSchema = yup .object({ type: yup.string().oneOf(['message']).required(), data: yup @@ -53,14 +53,14 @@ export const ClientSocketRpcSchema: yup.ObjectSchema = yup }) .required() -export const ServerSocketRpcSchema: yup.ObjectSchema = yup +export const RpcSocketServerMessageSchema: yup.ObjectSchema = yup .object({ type: yup.string().oneOf(['message', 'malformedRequest', 'error', 'stream']).required(), - data: yup.mixed().required(), + data: yup.mixed().required(), }) .required() -export const SocketRpcErrorSchema: yup.ObjectSchema = yup +export const RpcSocketErrorSchema: yup.ObjectSchema = yup .object({ code: yup.string().defined(), message: yup.string().defined(), @@ -68,7 +68,7 @@ export const SocketRpcErrorSchema: yup.ObjectSchema = yup }) .defined() -export const SocketRpcRequestSchema: yup.ObjectSchema = yup +export const RpcSocketRequestSchema: yup.ObjectSchema = yup .object({ mid: yup.number().required(), type: yup.string().required(), @@ -77,7 +77,7 @@ export const SocketRpcRequestSchema: yup.ObjectSchema = yup }) .required() -export const SocketRpcResponseSchema: yup.ObjectSchema = yup +export const RpcSocketResponseSchema: yup.ObjectSchema = yup .object({ id: yup.number().defined(), status: yup.number().defined(), @@ -85,7 +85,7 @@ export const SocketRpcResponseSchema: yup.ObjectSchema = yup }) .defined() -export const SocketRpcStreamSchema: yup.ObjectSchema = yup +export const RpcSocketStreamSchema: yup.ObjectSchema = yup .object({ id: yup.number().defined(), data: yup.mixed().notRequired(), diff --git a/ironfish/src/rpc/adapters/socketAdapter/socketAdapter.ts b/ironfish/src/rpc/adapters/socketAdapter/socketAdapter.ts index 770b5f08c4..b13cd19c9f 100644 --- a/ironfish/src/rpc/adapters/socketAdapter/socketAdapter.ts +++ b/ironfish/src/rpc/adapters/socketAdapter/socketAdapter.ts @@ -14,15 +14,15 @@ import { RpcRequest } from '../../request' import { ApiNamespace, Router } from '../../routes' import { RpcServer } from '../../server' import { IRpcAdapter } from '../adapter' -import { ERROR_CODES, ResponseError } from '../errors' +import { ERROR_CODES, RpcResponseError } from '../errors' import { - ClientSocketRpcSchema, MESSAGE_DELIMITER, - ServerSocketRpc, - SocketRpcError, + RpcSocketClientMessageSchema, + RpcSocketError, + RpcSocketServerMessage, } from './protocol' -type SocketClient = { +type RpcSocketClient = { id: string socket: net.Socket requests: Map @@ -38,7 +38,7 @@ export abstract class RpcSocketAdapter implements IRpcAdapter { enableAuthentication = true started = false - clients = new Map() + clients = new Map() inboundTraffic = new Meter() outboundTraffic = new Meter() @@ -155,7 +155,7 @@ export abstract class RpcSocketAdapter implements IRpcAdapter { await Promise.all(clients.map((c) => this.waitForClientToDisconnect(c))) } - waitForClientToDisconnect(client: SocketClient): Promise { + waitForClientToDisconnect(client: RpcSocketClient): Promise { return new Promise((resolve) => { client.socket.once('close', () => { resolve() @@ -183,17 +183,17 @@ export abstract class RpcSocketAdapter implements IRpcAdapter { }) } - onClientDisconnection(client: SocketClient): void { + onClientDisconnection(client: RpcSocketClient): void { client.requests.forEach((req) => req.close()) this.clients.delete(client.id) this.logger.debug(`client connection closed: ${this.describe()}`) } - onClientError(client: SocketClient, error: unknown): void { + onClientError(client: RpcSocketClient, error: unknown): void { this.logger.debug(`${this.describe()} has error: ${ErrorUtils.renderError(error)}`) } - async onClientData(client: SocketClient, data: Buffer): Promise { + async onClientData(client: RpcSocketClient, data: Buffer): Promise { this.inboundTraffic.add(data.byteLength) client.messageBuffer.write(data) @@ -204,7 +204,7 @@ export abstract class RpcSocketAdapter implements IRpcAdapter { return } - const result = await YupUtils.tryValidate(ClientSocketRpcSchema, parsed) + const result = await YupUtils.tryValidate(RpcSocketClientMessageSchema, parsed) if (result.error) { this.emitResponse(client, this.constructMalformedRequest(parsed)) @@ -228,7 +228,7 @@ export abstract class RpcSocketAdapter implements IRpcAdapter { try { if (this.router == null || this.router.server == null) { - throw new ResponseError('Tried to connect to unmounted adapter') + throw new RpcResponseError('Tried to connect to unmounted adapter') } // Authentication @@ -239,13 +239,13 @@ export abstract class RpcSocketAdapter implements IRpcAdapter { const error = message.auth ? 'Failed authentication' : 'Missing authentication token' - throw new ResponseError(error, ERROR_CODES.UNAUTHENTICATED, 401) + throw new RpcResponseError(error, ERROR_CODES.UNAUTHENTICATED, 401) } } await this.router.route(message.type, request) } catch (error: unknown) { - if (error instanceof ResponseError) { + if (error instanceof RpcResponseError) { const response = this.constructMessage(message.mid, error.status, { code: error.code, message: error.message, @@ -261,7 +261,11 @@ export abstract class RpcSocketAdapter implements IRpcAdapter { } } - emitResponse(client: SocketClient, data: ServerSocketRpc, requestId?: string): void { + emitResponse( + client: RpcSocketClient, + data: RpcSocketServerMessage, + requestId?: string, + ): void { const message = this.encodeMessage(data) client.socket.write(message) this.outboundTraffic.add(message.byteLength) @@ -272,17 +276,17 @@ export abstract class RpcSocketAdapter implements IRpcAdapter { } } - emitStream(client: SocketClient, data: ServerSocketRpc): void { + emitStream(client: RpcSocketClient, data: RpcSocketServerMessage): void { const message = this.encodeMessage(data) client.socket.write(message) this.outboundTraffic.add(message.byteLength) } - encodeMessage(data: ServerSocketRpc): Buffer { + encodeMessage(data: RpcSocketServerMessage): Buffer { return Buffer.from(JSON.stringify(data) + MESSAGE_DELIMITER) } - constructMessage(messageId: number, status: number, data: unknown): ServerSocketRpc { + constructMessage(messageId: number, status: number, data: unknown): RpcSocketServerMessage { return { type: 'message', data: { @@ -293,7 +297,7 @@ export abstract class RpcSocketAdapter implements IRpcAdapter { } } - constructStream(messageId: number, data: unknown): ServerSocketRpc { + constructStream(messageId: number, data: unknown): RpcSocketServerMessage { return { type: 'stream', data: { @@ -303,10 +307,10 @@ export abstract class RpcSocketAdapter implements IRpcAdapter { } } - constructMalformedRequest(request: unknown): ServerSocketRpc { + constructMalformedRequest(request: unknown): RpcSocketServerMessage { const error = new Error(`Malformed request rejected`) - const data: SocketRpcError = { + const data: RpcSocketError = { code: ERROR_CODES.ERROR, message: error.message, stack: error.stack, diff --git a/ironfish/src/rpc/adapters/tcpAdapter.test.ts b/ironfish/src/rpc/adapters/tcpAdapter.test.ts index 6b6c5e1a81..fb24e9fe91 100644 --- a/ironfish/src/rpc/adapters/tcpAdapter.test.ts +++ b/ironfish/src/rpc/adapters/tcpAdapter.test.ts @@ -13,7 +13,7 @@ import { getUniqueTestDataDir } from '../../testUtilities' import { RpcRequestError } from '../clients' import { RpcTcpClient } from '../clients/tcpClient' import { ALL_API_NAMESPACES } from '../routes' -import { ERROR_CODES, ValidationError } from './errors' +import { ERROR_CODES, RpcValidationError } from './errors' import { RpcTcpAdapter } from './tcpAdapter' describe('TcpAdapter', () => { @@ -97,7 +97,7 @@ describe('TcpAdapter', () => { Assert.isNotNull(tcp?.router) tcp.router.routes.register('foo/bar', yup.object({}), () => { - throw new ValidationError('hello error', 402, 'hello-error' as ERROR_CODES) + throw new RpcValidationError('hello error', 402, 'hello-error' as ERROR_CODES) }) client = new RpcTcpClient('localhost', 0) diff --git a/ironfish/src/rpc/clients/errors.ts b/ironfish/src/rpc/clients/errors.ts index 25ec354a60..2df66e7b9f 100644 --- a/ironfish/src/rpc/clients/errors.ts +++ b/ironfish/src/rpc/clients/errors.ts @@ -58,7 +58,7 @@ export class RpcRequestError extends Error { } /** Thrown when the request timeout has been exceeded and the request has been aborted */ -export class RequestTimeoutError extends RpcRequestError { +export class RpcRequestTimeoutError extends RpcRequestError { constructor(response: RpcResponse, timeoutMs: number, route: string) { super(response, 'request-timeout', `Timeout of ${timeoutMs} exceeded to ${route}`) } diff --git a/ironfish/src/rpc/clients/socketClient.ts b/ironfish/src/rpc/clients/socketClient.ts index 8755b5f36d..b5dab405dc 100644 --- a/ironfish/src/rpc/clients/socketClient.ts +++ b/ironfish/src/rpc/clients/socketClient.ts @@ -8,22 +8,22 @@ import { Logger } from '../../logger' import { ErrorUtils, PromiseUtils, SetTimeoutToken, YupUtils } from '../../utils' import { MESSAGE_DELIMITER, - ServerSocketRpc, - ServerSocketRpcSchema, - SocketRpcErrorSchema, - SocketRpcResponseSchema, - SocketRpcStreamSchema, + RpcSocketErrorSchema, + RpcSocketResponseSchema, + RpcSocketServerMessage, + RpcSocketServerMessageSchema, + RpcSocketStreamSchema, } from '../adapters' import { MessageBuffer } from '../messageBuffer' import { isRpcResponseError, RpcResponse } from '../response' import { Stream } from '../stream' import { RpcClient } from './client' import { - RequestTimeoutError, RpcConnectionError, RpcConnectionLostError, RpcConnectionRefusedError, RpcRequestError, + RpcRequestTimeoutError, } from './errors' export type RpcSocketClientConnectionInfo = { @@ -155,7 +155,7 @@ export abstract class RpcSocketClient extends RpcClient { const message = this.pending.get(messageId) if (message && response) { - message.reject(new RequestTimeoutError(response, timeoutMs, route)) + message.reject(new RpcRequestTimeoutError(response, timeoutMs, route)) } }, timeoutMs) } @@ -216,7 +216,7 @@ export abstract class RpcSocketClient extends RpcClient { } protected handleStream = async (data: unknown): Promise => { - const { result, error } = await YupUtils.tryValidate(SocketRpcStreamSchema, data) + const { result, error } = await YupUtils.tryValidate(RpcSocketStreamSchema, data) if (!result) { throw error } @@ -245,7 +245,7 @@ export abstract class RpcSocketClient extends RpcClient { } protected handleEnd = async (data: unknown): Promise => { - const { result, error } = await YupUtils.tryValidate(SocketRpcResponseSchema, data) + const { result, error } = await YupUtils.tryValidate(RpcSocketResponseSchema, data) if (!result) { throw error } @@ -259,7 +259,7 @@ export abstract class RpcSocketClient extends RpcClient { if (isRpcResponseError(pending.response)) { const { result: errorBody, error: errorError } = await YupUtils.tryValidate( - SocketRpcErrorSchema, + RpcSocketErrorSchema, result.data, ) @@ -299,13 +299,13 @@ export abstract class RpcSocketClient extends RpcClient { for (const message of this.messageBuffer.readMessages()) { const { result, error } = await YupUtils.tryValidate( - ServerSocketRpcSchema, + RpcSocketServerMessageSchema, JSON.parse(message), ) if (!result) { throw error } - const { type, data }: ServerSocketRpc = result + const { type, data }: RpcSocketServerMessage = result switch (type) { case 'message': { this.onMessage(data) diff --git a/ironfish/src/rpc/routes/chain/broadcastTransaction.ts b/ironfish/src/rpc/routes/chain/broadcastTransaction.ts index fb7ed23322..ec1a6b47a3 100644 --- a/ironfish/src/rpc/routes/chain/broadcastTransaction.ts +++ b/ironfish/src/rpc/routes/chain/broadcastTransaction.ts @@ -6,7 +6,7 @@ import * as yup from 'yup' import { Assert } from '../../../assert' import { FullNode } from '../../../node' import { Transaction } from '../../../primitives' -import { ValidationError } from '../../adapters' +import { RpcValidationError } from '../../adapters' import { ApiNamespace } from '../namespaces' import { routes } from '../router' @@ -47,7 +47,7 @@ routes.register( const id = Buffer.from(request.data.id, 'hex') if (id.byteLength !== ASSET_ID_LENGTH) { - throw new ValidationError( + throw new RpcValidationError( `Asset identifier is invalid length, expected ${ASSET_ID_LENGTH} but got ${id.byteLength}`, ) } const asset = await node.chain.getAssetById(id) if (!asset) { - throw new NotFoundError(`No asset found with identifier ${request.data.id}`) + throw new RpcNotFoundError(`No asset found with identifier ${request.data.id}`) } request.end({ diff --git a/ironfish/src/rpc/routes/chain/getBlock.ts b/ironfish/src/rpc/routes/chain/getBlock.ts index 63c1367893..6b1d3a66a5 100644 --- a/ironfish/src/rpc/routes/chain/getBlock.ts +++ b/ironfish/src/rpc/routes/chain/getBlock.ts @@ -8,7 +8,7 @@ import { FullNode } from '../../../node' import { BlockHeader } from '../../../primitives' import { GENESIS_BLOCK_SEQUENCE } from '../../../primitives/block' import { BufferUtils } from '../../../utils' -import { NotFoundError, ValidationError } from '../../adapters' +import { RpcNotFoundError, RpcValidationError } from '../../adapters' import { ApiNamespace } from '../namespaces' import { routes } from '../router' import { RpcBlock, RpcBlockSchema, serializeRpcBlockHeader } from '../types' @@ -94,16 +94,16 @@ routes.register( } if (!header) { - throw new NotFoundError(error) + throw new RpcNotFoundError(error) } if (header.noteSize === null) { - throw new ValidationError('Block header was saved to database without a note size') + throw new RpcValidationError('Block header was saved to database without a note size') } const block = await context.chain.getBlock(header) if (!block) { - throw new NotFoundError(`No block with header ${header.hash.toString('hex')}`) + throw new RpcNotFoundError(`No block with header ${header.hash.toString('hex')}`) } const transactions: GetBlockResponse['block']['transactions'] = [] diff --git a/ironfish/src/rpc/routes/chain/getDifficulty.ts b/ironfish/src/rpc/routes/chain/getDifficulty.ts index 1781720bcf..722fdf624d 100644 --- a/ironfish/src/rpc/routes/chain/getDifficulty.ts +++ b/ironfish/src/rpc/routes/chain/getDifficulty.ts @@ -4,7 +4,7 @@ import * as yup from 'yup' import { Assert } from '../../../assert' import { FullNode } from '../../../node' -import { ValidationError } from '../../adapters' +import { RpcValidationError } from '../../adapters' import { ApiNamespace } from '../namespaces' import { routes } from '../router' @@ -46,7 +46,7 @@ routes.register( if (request.data?.sequence) { const sequenceBlock = await node.chain.getHeaderAtSequence(request.data.sequence) if (!sequenceBlock) { - throw new ValidationError(`No block found at sequence ${request.data.sequence}`) + throw new RpcValidationError(`No block found at sequence ${request.data.sequence}`) } sequence = sequenceBlock.sequence block = sequenceBlock diff --git a/ironfish/src/rpc/routes/chain/getNetworkHashPower.ts b/ironfish/src/rpc/routes/chain/getNetworkHashPower.ts index 2bd5b6de2f..dd3f5c2416 100644 --- a/ironfish/src/rpc/routes/chain/getNetworkHashPower.ts +++ b/ironfish/src/rpc/routes/chain/getNetworkHashPower.ts @@ -5,7 +5,7 @@ import * as yup from 'yup' import { Assert } from '../../../assert' import { FullNode } from '../../../node' import { BigIntUtils } from '../../../utils' -import { ValidationError } from '../../adapters' +import { RpcValidationError } from '../../adapters' import { ApiNamespace } from '../namespaces' import { routes } from '../router' @@ -47,7 +47,7 @@ routes.register( const witness = await chain.notes.witness(request.data.index, maxConfirmedHeader.noteSize) if (witness === null) { - throw new ValidationError( + throw new RpcValidationError( `No confirmed notes exist with index ${request.data.index} in tree of size ${maxConfirmedHeader.noteSize}`, ) } diff --git a/ironfish/src/rpc/routes/chain/getTransaction.ts b/ironfish/src/rpc/routes/chain/getTransaction.ts index 755c7ced31..3719ba01a5 100644 --- a/ironfish/src/rpc/routes/chain/getTransaction.ts +++ b/ironfish/src/rpc/routes/chain/getTransaction.ts @@ -7,7 +7,7 @@ import { getTransactionSize } from '../../../network/utils/serializers' import { FullNode } from '../../../node' import { BlockHashSerdeInstance } from '../../../serde' import { CurrencyUtils } from '../../../utils' -import { NotFoundError, ValidationError } from '../../adapters' +import { RpcNotFoundError, RpcValidationError } from '../../adapters' import { ApiNamespace } from '../namespaces' import { routes } from '../router' import { RpcTransaction, RpcTransactionSchema } from './types' @@ -58,7 +58,7 @@ routes.register( Assert.isInstanceOf(context, FullNode) if (!request.data.transactionHash) { - throw new ValidationError(`Missing transaction hash`) + throw new RpcValidationError(`Missing transaction hash`) } const transactionHashBuffer = Buffer.from(request.data.transactionHash, 'hex') @@ -68,14 +68,14 @@ routes.register( : await context.chain.getBlockHashByTransactionHash(transactionHashBuffer) if (!blockHashBuffer) { - throw new NotFoundError( + throw new RpcNotFoundError( `No block hash found for transaction hash ${request.data.transactionHash}`, ) } const blockHeader = await context.chain.getHeader(blockHashBuffer) if (!blockHeader) { - throw new NotFoundError( + throw new RpcNotFoundError( `No block found for block hash ${blockHashBuffer.toString('hex')}`, ) } @@ -87,7 +87,7 @@ routes.register( ) if (!foundTransaction) { - throw new NotFoundError( + throw new RpcNotFoundError( `Transaction not found on block ${blockHashBuffer.toString('hex')}`, ) } diff --git a/ironfish/src/rpc/routes/chain/getTransactionStream.ts b/ironfish/src/rpc/routes/chain/getTransactionStream.ts index 492b53862b..1d33335ad4 100644 --- a/ironfish/src/rpc/routes/chain/getTransactionStream.ts +++ b/ironfish/src/rpc/routes/chain/getTransactionStream.ts @@ -10,7 +10,7 @@ import { BlockHeader } from '../../../primitives/blockheader' import { BufferUtils, CurrencyUtils } from '../../../utils' import { PromiseUtils } from '../../../utils/promise' import { isValidIncomingViewKey, isValidOutgoingViewKey } from '../../../wallet/validator' -import { ValidationError } from '../../adapters/errors' +import { RpcValidationError } from '../../adapters/errors' import { ApiNamespace } from '../namespaces' import { routes } from '../router' import { @@ -112,17 +112,17 @@ routes.register( AssertHasRpcContext(request, context, 'config') if (request.data?.name && !(request.data.name in context.config.defaults)) { - throw new ValidationError(`No config option ${String(request.data.name)}`) + throw new RpcValidationError(`No config option ${String(request.data.name)}`) } let pickKeys: string[] | undefined = undefined diff --git a/ironfish/src/rpc/routes/config/uploadConfig.ts b/ironfish/src/rpc/routes/config/uploadConfig.ts index 5c6a64ba36..6d280c83f1 100644 --- a/ironfish/src/rpc/routes/config/uploadConfig.ts +++ b/ironfish/src/rpc/routes/config/uploadConfig.ts @@ -3,7 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import * as yup from 'yup' import { Config, ConfigOptions, ConfigOptionsSchema } from '../../../fileStores/config' -import { ValidationError } from '../../adapters/errors' +import { RpcValidationError } from '../../adapters/errors' import { ApiNamespace } from '../namespaces' import { routes } from '../router' import { AssertHasRpcContext } from '../rpcContext' @@ -52,7 +52,7 @@ export function setUnknownConfigValue( ): void { if (unknownKey && !(unknownKey in config.defaults)) { if (!ignoreUnknownKey) { - throw new ValidationError(`No config option ${String(unknownKey)}`) + throw new RpcValidationError(`No config option ${String(unknownKey)}`) } } @@ -99,7 +99,7 @@ function stringToStringArray(value: string): string[] | null { function convertValue(sourceKey: string, sourceValue: unknown, targetValue: unknown): unknown { if (typeof sourceValue !== 'string') { - throw new ValidationError( + throw new RpcValidationError( `${sourceKey} has an invalid value: Cannot convert ${JSON.stringify( sourceValue, )} from ${typeof sourceValue} to ${String(typeof targetValue)}`, @@ -139,7 +139,7 @@ function convertValue(sourceKey: string, sourceValue: unknown, targetValue: unkn targetType = 'array' } - throw new ValidationError( + throw new RpcValidationError( `${sourceKey} has an invalid value: Could not convert ${JSON.stringify( sourceValue, )} from ${typeof sourceValue} to ${String(targetType)}`, diff --git a/ironfish/src/rpc/routes/faucet/getFunds.ts b/ironfish/src/rpc/routes/faucet/getFunds.ts index b559c3788d..fc0f40ffd3 100644 --- a/ironfish/src/rpc/routes/faucet/getFunds.ts +++ b/ironfish/src/rpc/routes/faucet/getFunds.ts @@ -4,7 +4,7 @@ import axios, { AxiosError } from 'axios' import * as yup from 'yup' import { Assert } from '../../../assert' -import { ERROR_CODES, ResponseError } from '../../adapters' +import { ERROR_CODES, RpcResponseError } from '../../adapters' import { ApiNamespace } from '../namespaces' import { routes } from '../router' import { AssertHasRpcContext } from '../rpcContext' @@ -37,7 +37,10 @@ routes.register( if (networkId !== 0) { // not testnet - throw new ResponseError('This endpoint is only available for testnet.', ERROR_CODES.ERROR) + throw new RpcResponseError( + 'This endpoint is only available for testnet.', + ERROR_CODES.ERROR, + ) } const account = getAccount(context.wallet, request.data.account) @@ -54,20 +57,20 @@ routes.register( if (status === 422) { if (data.code === 'faucet_max_requests_reached') { Assert.isNotUndefined(data.message) - throw new ResponseError(data.message, ERROR_CODES.VALIDATION, status) + throw new RpcResponseError(data.message, ERROR_CODES.VALIDATION, status) } - throw new ResponseError( + throw new RpcResponseError( 'You entered an invalid email.', ERROR_CODES.VALIDATION, status, ) } else if (data.message) { - throw new ResponseError(data.message, ERROR_CODES.ERROR, status) + throw new RpcResponseError(data.message, ERROR_CODES.ERROR, status) } } - throw new ResponseError(error.message, ERROR_CODES.ERROR, Number(error.code)) + throw new RpcResponseError(error.message, ERROR_CODES.ERROR, Number(error.code)) }) request.end({ diff --git a/ironfish/src/rpc/routes/router.ts b/ironfish/src/rpc/routes/router.ts index e995ba17f4..aaf93a359f 100644 --- a/ironfish/src/rpc/routes/router.ts +++ b/ironfish/src/rpc/routes/router.ts @@ -5,7 +5,7 @@ import { Assert } from '../../assert' import { YupSchema, YupSchemaResult, YupUtils } from '../../utils' import { StrEnumUtils } from '../../utils/enums' import { ERROR_CODES } from '../adapters' -import { ResponseError, ValidationError } from '../adapters/errors' +import { RpcResponseError, RpcValidationError } from '../adapters/errors' import { RpcRequest } from '../request' import { RpcServer } from '../server' import { ApiNamespace } from './namespaces' @@ -18,7 +18,7 @@ export type RouteHandler = ( context: RpcContext, ) => Promise | void -export class RouteNotFoundError extends ResponseError { +export class RouteNotFoundError extends RpcResponseError { constructor(route: string, namespace: string, method: string) { super( `No route found ${route} in namespace ${namespace} for method ${method}`, @@ -56,18 +56,18 @@ export class Router { const { result, error } = await YupUtils.tryValidate(schema, request.data) if (error) { - throw new ValidationError(error.message, 400) + throw new RpcValidationError(error.message, 400) } request.data = result try { await handler(request, this.server.context) } catch (e: unknown) { - if (e instanceof ResponseError) { + if (e instanceof RpcResponseError) { throw e } if (e instanceof Error) { - throw new ResponseError(e) + throw new RpcResponseError(e) } throw e } diff --git a/ironfish/src/rpc/routes/wallet/addTransaction.ts b/ironfish/src/rpc/routes/wallet/addTransaction.ts index 57404d48a7..fa377ca439 100644 --- a/ironfish/src/rpc/routes/wallet/addTransaction.ts +++ b/ironfish/src/rpc/routes/wallet/addTransaction.ts @@ -5,7 +5,7 @@ import * as yup from 'yup' import { Verifier } from '../../../consensus' import { Transaction } from '../../../primitives' import { AsyncUtils } from '../../../utils' -import { ValidationError } from '../../adapters' +import { RpcValidationError } from '../../adapters' import { ApiNamespace } from '../namespaces' import { routes } from '../router' import { AssertHasRpcContext } from '../rpcContext' @@ -48,7 +48,7 @@ routes.register( const verify = Verifier.verifyCreatedTransaction(transaction, context.strategy.consensus) if (!verify.valid) { - throw new ValidationError(`Invalid transaction, reason: ${String(verify.reason)}`, 400) + throw new RpcValidationError(`Invalid transaction, reason: ${String(verify.reason)}`, 400) } await context.wallet.addPendingTransaction(transaction) @@ -58,7 +58,7 @@ routes.register( ) if (accounts.length === 0) { - throw new ValidationError( + throw new RpcValidationError( `Transaction ${transaction.hash().toString('hex')} is not related to any account`, ) } diff --git a/ironfish/src/rpc/routes/wallet/create.ts b/ironfish/src/rpc/routes/wallet/create.ts index c1cf9d6de9..ff780555b5 100644 --- a/ironfish/src/rpc/routes/wallet/create.ts +++ b/ironfish/src/rpc/routes/wallet/create.ts @@ -8,7 +8,7 @@ * is the verbObject naming convention. For example, `POST /wallet/burnAsset` burns an asset. */ -import { ERROR_CODES, ValidationError } from '../../adapters' +import { ERROR_CODES, RpcValidationError } from '../../adapters' import { ApiNamespace } from '../namespaces' import { routes } from '../router' import { AssertHasRpcContext } from '../rpcContext' @@ -23,7 +23,7 @@ routes.register( const name = request.data.name if (context.wallet.accountExists(name)) { - throw new ValidationError( + throw new RpcValidationError( `There is already an account with the name ${name}`, 400, ERROR_CODES.ACCOUNT_EXISTS, diff --git a/ironfish/src/rpc/routes/wallet/createAccount.ts b/ironfish/src/rpc/routes/wallet/createAccount.ts index 11d1387294..5ec0cf76c0 100644 --- a/ironfish/src/rpc/routes/wallet/createAccount.ts +++ b/ironfish/src/rpc/routes/wallet/createAccount.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 * as yup from 'yup' -import { ERROR_CODES, ValidationError } from '../../adapters' +import { ERROR_CODES, RpcValidationError } from '../../adapters' import { ApiNamespace } from '../namespaces' import { routes } from '../router' import { AssertHasRpcContext } from '../rpcContext' @@ -48,7 +48,7 @@ routes.register( const name = request.data.name if (context.wallet.accountExists(name)) { - throw new ValidationError( + throw new RpcValidationError( `There is already an account with the name ${name}`, 400, ERROR_CODES.ACCOUNT_EXISTS, diff --git a/ironfish/src/rpc/routes/wallet/createTransaction.ts b/ironfish/src/rpc/routes/wallet/createTransaction.ts index 0ddb8094ef..0e78b082ed 100644 --- a/ironfish/src/rpc/routes/wallet/createTransaction.ts +++ b/ironfish/src/rpc/routes/wallet/createTransaction.ts @@ -13,7 +13,7 @@ import { RawTransactionSerde } from '../../../primitives/rawTransaction' import { CurrencyUtils, YupUtils } from '../../../utils' import { Wallet } from '../../../wallet' import { NotEnoughFundsError } from '../../../wallet/errors' -import { ERROR_CODES, ValidationError } from '../../adapters/errors' +import { ERROR_CODES, RpcValidationError } from '../../adapters/errors' import { ApiNamespace } from '../namespaces' import { routes } from '../router' import { AssertHasRpcContext } from '../rpcContext' @@ -136,7 +136,7 @@ routes.register( const id = Buffer.from(request.data.id, 'hex') if (id.byteLength !== ASSET_ID_LENGTH) { - throw new ValidationError( + throw new RpcValidationError( `Asset identifier is invalid length, expected ${ASSET_ID_LENGTH} but got ${id.byteLength}`, ) } const asset = await account.getAsset(id) if (!asset) { - throw new NotFoundError(`No asset found with identifier ${request.data.id}`) + throw new RpcNotFoundError(`No asset found with identifier ${request.data.id}`) } request.end({ diff --git a/ironfish/src/rpc/routes/wallet/getNodeStatus.ts b/ironfish/src/rpc/routes/wallet/getNodeStatus.ts index 6e7c38277f..d5598527a3 100644 --- a/ironfish/src/rpc/routes/wallet/getNodeStatus.ts +++ b/ironfish/src/rpc/routes/wallet/getNodeStatus.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 { Assert } from '../../../assert' -import { ValidationError } from '../../adapters' +import { RpcValidationError } from '../../adapters' import { RpcSocketClient } from '../../clients' import { ApiNamespace } from '../namespaces' import { GetNodeStatusResponse, GetStatusRequestSchema } from '../node/getStatus' @@ -16,7 +16,7 @@ routes.register( Assert.isNotNull(wallet.nodeClient) if (wallet.nodeClient instanceof RpcSocketClient && !wallet.nodeClient.isConnected) { - throw new ValidationError('Wallet node client is disconnected') + throw new RpcValidationError('Wallet node client is disconnected') } if (!request.data?.stream) { diff --git a/ironfish/src/rpc/routes/wallet/mintAsset.ts b/ironfish/src/rpc/routes/wallet/mintAsset.ts index 11a03b0894..bb8dab4a76 100644 --- a/ironfish/src/rpc/routes/wallet/mintAsset.ts +++ b/ironfish/src/rpc/routes/wallet/mintAsset.ts @@ -10,7 +10,7 @@ import * as yup from 'yup' import { Assert } from '../../../assert' import { CurrencyUtils, YupUtils } from '../../../utils' import { MintAssetOptions } from '../../../wallet/interfaces/mintAssetOptions' -import { ValidationError } from '../../adapters' +import { RpcValidationError } from '../../adapters' import { ApiNamespace } from '../namespaces' import { routes } from '../router' import { AssertHasRpcContext } from '../rpcContext' @@ -92,7 +92,7 @@ routes.register( request.data.transferOwnershipTo && !isValidPublicAddress(request.data.transferOwnershipTo) ) { - throw new ValidationError('transferOwnershipTo must be a valid public address') + throw new RpcValidationError('transferOwnershipTo must be a valid public address') } let options: MintAssetOptions diff --git a/ironfish/src/rpc/routes/wallet/rescanAccount.ts b/ironfish/src/rpc/routes/wallet/rescanAccount.ts index 6de98b4a77..4bfa80ca25 100644 --- a/ironfish/src/rpc/routes/wallet/rescanAccount.ts +++ b/ironfish/src/rpc/routes/wallet/rescanAccount.ts @@ -3,7 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import * as yup from 'yup' import { GENESIS_BLOCK_SEQUENCE } from '../../../primitives' -import { ValidationError } from '../../adapters/errors' +import { RpcValidationError } from '../../adapters/errors' import { ApiNamespace } from '../namespaces' import { routes } from '../router' import { AssertHasRpcContext } from '../rpcContext' @@ -35,7 +35,7 @@ routes.register( let scan = context.wallet.scan if (scan && !request.data.follow) { - throw new ValidationError(`A transaction rescan is already running`) + throw new RpcValidationError(`A transaction rescan is already running`) } if (!scan) { @@ -50,7 +50,7 @@ routes.register( const response = await context.wallet.chainGetBlock({ sequence: request.data.from }) if (response === null) { - throw new ValidationError( + throw new RpcValidationError( `No block header found in the chain at sequence ${request.data.from}`, ) } diff --git a/ironfish/src/rpc/routes/wallet/sendTransaction.ts b/ironfish/src/rpc/routes/wallet/sendTransaction.ts index 464e198e01..ba9616c1ee 100644 --- a/ironfish/src/rpc/routes/wallet/sendTransaction.ts +++ b/ironfish/src/rpc/routes/wallet/sendTransaction.ts @@ -8,7 +8,7 @@ import { Assert } from '../../../assert' import { CurrencyUtils, YupUtils } from '../../../utils' import { Wallet } from '../../../wallet' import { NotEnoughFundsError } from '../../../wallet/errors' -import { ERROR_CODES, ValidationError } from '../../adapters/errors' +import { ERROR_CODES, RpcValidationError } from '../../adapters/errors' import { ApiNamespace } from '../namespaces' import { routes } from '../router' import { AssertHasRpcContext } from '../rpcContext' @@ -80,7 +80,7 @@ routes.register( const status = await context.wallet.nodeClient.node.getStatus() if (!status.content.blockchain.synced) { - throw new ValidationError( + throw new RpcValidationError( `Your node must be synced with the Iron Fish network to send a transaction. Please try again later`, ) } @@ -123,7 +123,7 @@ routes.register( }) if (balance.available < sum) { - throw new ValidationError( + throw new RpcValidationError( `Your balance is too low. Add funds to your account first`, undefined, ERROR_CODES.INSUFFICIENT_BALANCE, @@ -145,7 +145,7 @@ routes.register( }) } catch (e) { if (e instanceof NotEnoughFundsError) { - throw new ValidationError(e.message, 400, ERROR_CODES.INSUFFICIENT_BALANCE) + throw new RpcValidationError(e.message, 400, ERROR_CODES.INSUFFICIENT_BALANCE) } throw e } diff --git a/ironfish/src/rpc/routes/wallet/utils.ts b/ironfish/src/rpc/routes/wallet/utils.ts index 2512e56e5f..ff3cc68b25 100644 --- a/ironfish/src/rpc/routes/wallet/utils.ts +++ b/ironfish/src/rpc/routes/wallet/utils.ts @@ -10,7 +10,7 @@ import { AssetValue } from '../../../wallet/walletdb/assetValue' import { DecryptedNoteValue } from '../../../wallet/walletdb/decryptedNoteValue' import { TransactionValue } from '../../../wallet/walletdb/transactionValue' import { WorkerPool } from '../../../workerPool' -import { ValidationError } from '../../adapters' +import { RpcValidationError } from '../../adapters' import { RpcAccountAssetBalanceDelta, RpcAccountImport, @@ -25,7 +25,7 @@ export function getAccount(wallet: Wallet, name?: string): Account { if (account) { return account } - throw new ValidationError(`No account with name ${name}`) + throw new RpcValidationError(`No account with name ${name}`) } const defaultAccount = wallet.getDefaultAccount() @@ -33,7 +33,7 @@ export function getAccount(wallet: Wallet, name?: string): Account { return defaultAccount } - throw new ValidationError( + throw new RpcValidationError( `No account is currently active.\n\n` + `Use ironfish wallet:create to first create an account`, ) From acfec3fb7d6273b02ea9e050f70bd6225241f936 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Fri, 19 Jan 2024 16:05:11 -0800 Subject: [PATCH 27/38] Move auth token into if statement (#4574) There's no reason to declare this variable in this scope. --- ironfish/src/sdk.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ironfish/src/sdk.ts b/ironfish/src/sdk.ts index 7202909c42..65477da523 100644 --- a/ironfish/src/sdk.ts +++ b/ironfish/src/sdk.ts @@ -147,10 +147,11 @@ export class IronfishSdk { } let client: RpcSocketClient - const rpcAuthToken = internal.get('rpcAuthToken') if (config.get('enableRpcTcp')) { if (config.get('enableRpcTls')) { + const rpcAuthToken = internal.get('rpcAuthToken') + client = new RpcTlsClient( config.get('rpcTcpHost'), config.get('rpcTcpPort'), From b23d9f340967e9bc5331fff99e2c031ebfee09c7 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Fri, 19 Jan 2024 16:05:19 -0800 Subject: [PATCH 28/38] Default timeout to null (#4575) This is optional and you don't need to pass it in --- ironfish/src/rpc/response.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ironfish/src/rpc/response.ts b/ironfish/src/rpc/response.ts index 523a695261..622f9c9ce2 100644 --- a/ironfish/src/rpc/response.ts +++ b/ironfish/src/rpc/response.ts @@ -31,7 +31,7 @@ export class RpcResponse { constructor( promise: Promise, stream: Stream, - timeout: SetTimeoutToken | null, + timeout: SetTimeoutToken | null = null, ) { this.promise = promise this.stream = stream From 1b010cfe7830cc8c3478793c392491e245bcc11c Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Fri, 19 Jan 2024 16:45:04 -0800 Subject: [PATCH 29/38] Move weird await usage (#4577) not sure why this is on the next line --- ironfish/src/rpc/routes/node/getStatus.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ironfish/src/rpc/routes/node/getStatus.ts b/ironfish/src/rpc/routes/node/getStatus.ts index bcbbf88521..a9903ec2c4 100644 --- a/ironfish/src/rpc/routes/node/getStatus.ts +++ b/ironfish/src/rpc/routes/node/getStatus.ts @@ -262,8 +262,8 @@ routes.register( let stream = true while (stream) { - const status = getStatus(node) - request.stream(await status) + const status = await getStatus(node) + request.stream(status) await PromiseUtils.sleep(500) } From 54db84e0e66a7d1debba695b2e6b3703669cdcda Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Mon, 22 Jan 2024 09:35:01 -0800 Subject: [PATCH 30/38] Rahul/ifl 2044 split spender key (#4555) * split_spender_key * test stub * moving split_spender_key to frost_utils * publishing hash_viewing_key * removing unused import * spender key config * removing comment * no need to recreate the incoming viewkey * asserting keys are split correctly * directly calling thread rng * adding test for mismatch lengths * changing test name * adding partial eq to errors * adding frost error kind * split secret returns ironfish error type * updating tests with new error type * removing unwrap * changed split_secret to pub crate * variable name changes * vec to array helper * asserting error type * sorting vectors * fixing lint error * moving identifiers after assignment * adding test comment * changing split_secret to map to an ironfish error --- ironfish-rust/src/errors.rs | 10 +- ironfish-rust/src/frost_utils/mod.rs | 1 + ironfish-rust/src/frost_utils/split_secret.rs | 50 +++-- .../src/frost_utils/split_spender_key.rs | 196 ++++++++++++++++++ 4 files changed, 242 insertions(+), 15 deletions(-) create mode 100644 ironfish-rust/src/frost_utils/split_spender_key.rs diff --git a/ironfish-rust/src/errors.rs b/ironfish-rust/src/errors.rs index 90c809def3..0717b88156 100644 --- a/ironfish-rust/src/errors.rs +++ b/ironfish-rust/src/errors.rs @@ -22,10 +22,11 @@ pub struct IronfishError { /// in the code to reduce the cognitive load needed for using Result and Error /// types. The second is to give a singular type to convert into NAPI errors to /// be raised on the Javascript side. -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum IronfishErrorKind { BellpersonSynthesis, CryptoBox, + FrostLibError, IllegalValue, InconsistentWitness, InvalidAssetIdentifier, @@ -46,6 +47,7 @@ pub enum IronfishErrorKind { InvalidOutputProof, InvalidPaymentAddress, InvalidPublicAddress, + InvalidSecret, InvalidSignature, InvalidSigningKey, InvalidSpendProof, @@ -129,3 +131,9 @@ impl From for IronfishError { IronfishError::new_with_source(IronfishErrorKind::TryFromInt, e) } } + +impl From for IronfishError { + fn from(e: ironfish_frost::frost::Error) -> IronfishError { + IronfishError::new_with_source(IronfishErrorKind::FrostLibError, e) + } +} diff --git a/ironfish-rust/src/frost_utils/mod.rs b/ironfish-rust/src/frost_utils/mod.rs index fdc67c6e20..559dd5588e 100644 --- a/ironfish-rust/src/frost_utils/mod.rs +++ b/ironfish-rust/src/frost_utils/mod.rs @@ -5,3 +5,4 @@ pub mod round_one; pub mod round_two; pub mod split_secret; +pub mod split_spender_key; diff --git a/ironfish-rust/src/frost_utils/split_secret.rs b/ironfish-rust/src/frost_utils/split_secret.rs index 81c4788c79..42271de691 100644 --- a/ironfish-rust/src/frost_utils/split_secret.rs +++ b/ironfish-rust/src/frost_utils/split_secret.rs @@ -2,35 +2,36 @@ * 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_frost::frost; -use ironfish_frost::frost::Error; use ironfish_frost::frost::{ + frost::keys::split, keys::{IdentifierList, KeyPackage, PublicKeyPackage}, Identifier, SigningKey, }; use rand::rngs::ThreadRng; use std::collections::HashMap; +use crate::errors::{IronfishError, IronfishErrorKind}; + pub struct SecretShareConfig { pub min_signers: u16, pub max_signers: u16, pub secret: Vec, } -pub fn split_secret( +pub(crate) fn split_secret( config: &SecretShareConfig, identifiers: IdentifierList, rng: &mut ThreadRng, -) -> Result<(HashMap, PublicKeyPackage), Error> { - let secret_key = SigningKey::deserialize( - config - .secret - .clone() - .try_into() - .map_err(|_| Error::MalformedSigningKey)?, - )?; +) -> Result<(HashMap, PublicKeyPackage), IronfishError> { + let secret_bytes: [u8; 32] = config + .secret + .clone() + .try_into() + .map_err(|_| IronfishError::new(IronfishErrorKind::InvalidSecret))?; - let (shares, pubkeys) = frost::keys::split( + let secret_key = SigningKey::deserialize(secret_bytes)?; + + let (shares, pubkeys) = split( &secret_key, config.max_signers, config.min_signers, @@ -39,13 +40,13 @@ pub fn split_secret( )?; for (_k, v) in shares.clone() { - frost::keys::KeyPackage::try_from(v)?; + KeyPackage::try_from(v)?; } let mut key_packages: HashMap<_, _> = HashMap::new(); for (identifier, secret_share) in shares { - let key_package = frost::keys::KeyPackage::try_from(secret_share.clone()).unwrap(); + let key_package = KeyPackage::try_from(secret_share.clone())?; key_packages.insert(identifier, key_package); } @@ -58,6 +59,27 @@ mod test { use crate::keys::SaplingKey; use ironfish_frost::frost::{frost::keys::reconstruct, JubjubBlake2b512}; + #[test] + fn test_invalid_secret() { + let vec = vec![1; 31]; + let config = SecretShareConfig { + min_signers: 2, + max_signers: 3, + secret: vec, + }; + let mut rng = rand::thread_rng(); + let result = split_secret( + &config, + ironfish_frost::frost::keys::IdentifierList::Default, + &mut rng, + ); + assert!(result.is_err()); + assert!( + matches!(result.unwrap_err().kind, IronfishErrorKind::InvalidSecret), + "expected InvalidSecret error" + ); + } + #[test] fn test_split_secret() { let mut rng = rand::thread_rng(); diff --git a/ironfish-rust/src/frost_utils/split_spender_key.rs b/ironfish-rust/src/frost_utils/split_spender_key.rs new file mode 100644 index 0000000000..4f425770c5 --- /dev/null +++ b/ironfish-rust/src/frost_utils/split_spender_key.rs @@ -0,0 +1,196 @@ +/* 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 group::GroupEncoding; +use ironfish_frost::frost::{ + keys::{IdentifierList, KeyPackage, PublicKeyPackage}, + Identifier, +}; +use ironfish_zkp::{constants::PROOF_GENERATION_KEY_GENERATOR, ProofGenerationKey}; +use jubjub::SubgroupPoint; +use rand::thread_rng; +use std::collections::HashMap; + +use crate::{ + errors::{IronfishError, IronfishErrorKind}, + IncomingViewKey, OutgoingViewKey, PublicAddress, SaplingKey, ViewKey, +}; + +use super::split_secret::{split_secret, SecretShareConfig}; + +type AuthorizingKey = [u8; 32]; + +pub struct TrustedDealerKeyPackages { + pub verifying_key: AuthorizingKey, // verifying_key is the name given to this field in the frost protocol + pub proof_generation_key: ProofGenerationKey, + pub view_key: ViewKey, + pub incoming_view_key: IncomingViewKey, + pub outgoing_view_key: OutgoingViewKey, + pub public_address: PublicAddress, + pub key_packages: HashMap, + pub public_key_package: PublicKeyPackage, +} + +pub fn split_spender_key( + coordinator_sapling_key: SaplingKey, + min_signers: u16, + max_signers: u16, + identifiers: Vec, +) -> Result { + let secret = coordinator_sapling_key + .spend_authorizing_key + .to_bytes() + .to_vec(); + + let secret_config = SecretShareConfig { + min_signers, + max_signers, + secret, + }; + + let identifier_list = IdentifierList::Custom(&identifiers); + + let mut rng: rand::prelude::ThreadRng = thread_rng(); + + let (key_packages, public_key_package) = + split_secret(&secret_config, identifier_list, &mut rng)?; + + let authorizing_key_bytes = public_key_package.verifying_key().serialize(); + + let authorizing_key = Option::from(SubgroupPoint::from_bytes(&authorizing_key_bytes)) + .ok_or_else(|| IronfishError::new(IronfishErrorKind::InvalidAuthorizingKey))?; + + let proof_generation_key = ProofGenerationKey { + ak: authorizing_key, + nsk: coordinator_sapling_key.sapling_proof_generation_key().nsk, + }; + + let nullifier_deriving_key = *PROOF_GENERATION_KEY_GENERATOR + * coordinator_sapling_key.sapling_proof_generation_key().nsk; + + let view_key = ViewKey { + authorizing_key, + nullifier_deriving_key, + }; + + let incoming_view_key = coordinator_sapling_key.incoming_view_key().clone(); + + let outgoing_view_key: OutgoingViewKey = coordinator_sapling_key.outgoing_view_key().clone(); + + let public_address = incoming_view_key.public_address(); + + Ok(TrustedDealerKeyPackages { + verifying_key: authorizing_key_bytes, + proof_generation_key, + view_key, + incoming_view_key, + outgoing_view_key, + public_address, + key_packages, + public_key_package, + }) +} + +#[cfg(test)] +mod test { + use super::*; + use ironfish_frost::{ + frost::{frost::keys::reconstruct, JubjubBlake2b512}, + participant::Secret, + }; + + #[test] + fn test_throws_error_with_mismatch_signer_and_identifiers_lengths() { + let mut identifiers = Vec::new(); + + for _ in 0..10 { + identifiers.push( + Secret::random(thread_rng()) + .to_identity() + .to_frost_identifier(), + ); + } + + let sapling_key_1 = SaplingKey::generate_key(); + // max signers > identifiers length + let result = split_spender_key(sapling_key_1, 5, 11, identifiers.clone()); + assert!(result.is_err()); + let err = result.err().unwrap(); + assert_eq!(err.kind, IronfishErrorKind::FrostLibError); + assert!(err.to_string().contains("Incorrect number of identifiers.")); + + let sapling_key2 = SaplingKey::generate_key(); + // max signers < identifiers length + let result = split_spender_key(sapling_key2, 5, 9, identifiers.clone()); + + assert!(result.is_err()); + let err = result.err().unwrap(); + assert_eq!(err.kind, IronfishErrorKind::FrostLibError); + assert!(err.to_string().contains("Incorrect number of identifiers.")); + } + + #[test] + fn test_split_spender_key_success() { + let mut identifiers = Vec::new(); + + for _ in 0..10 { + identifiers.push( + Secret::random(thread_rng()) + .to_identity() + .to_frost_identifier(), + ); + } + let mut cloned_identifiers = identifiers.clone(); + cloned_identifiers.sort(); + + let sapling_key = SaplingKey::generate_key(); + + let trusted_dealer_key_packages = + split_spender_key(sapling_key.clone(), 5, 10, identifiers) + .expect("spender key split failed"); + + assert_eq!( + trusted_dealer_key_packages.key_packages.len(), + 10, + "should have 10 key packages" + ); + + assert_eq!( + trusted_dealer_key_packages.view_key.to_bytes(), + sapling_key.view_key.to_bytes(), + "should have the same incoming viewing key" + ); + + assert_eq!( + trusted_dealer_key_packages.public_address, + sapling_key.public_address(), + "should have the same public address" + ); + + let spend_auth_key = sapling_key.spend_authorizing_key.to_bytes(); + + let key_parts: Vec<_> = trusted_dealer_key_packages + .key_packages + .values() + .cloned() + .collect(); + + let signing_key = + reconstruct::(&key_parts).expect("key reconstruction failed"); + + let scalar = signing_key.to_scalar(); + + assert_eq!(scalar.to_bytes(), spend_auth_key); + + // assert identifiers and trusted_dealer_key_packages.key_packages.keys() are the same + let mut t_identifiers = trusted_dealer_key_packages + .key_packages + .keys() + .cloned() + .collect::>(); + + t_identifiers.sort(); + assert_eq!(t_identifiers, cloned_identifiers); + } +} From cd02987cffb34a0be3e4cabc158912c71dc5d2c3 Mon Sep 17 00:00:00 2001 From: jowparks Date: Mon, 22 Jan 2024 13:00:03 -0800 Subject: [PATCH 31/38] moves change notes construction inside of `build` function of `ProposedTransaction`. Makes it so no new method is required for build layer (#4572) --- ironfish-rust-nodejs/index.d.ts | 2 +- ironfish-rust-nodejs/src/structs/transaction.rs | 6 ++++++ ironfish-rust/src/transaction/mod.rs | 13 ++++++++++--- ironfish-rust/src/transaction/outputs.rs | 4 ++++ ironfish-rust/src/transaction/tests.rs | 6 ++---- 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index acb5fc40e8..4514adfd9a 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -223,7 +223,7 @@ export class Transaction { * aka: self.value_balance - intended_transaction_fee - change = 0 */ post(spenderHexKey: string, changeGoesTo: string | undefined | null, intendedTransactionFee: bigint): Buffer - build(proofGenerationKeyStr: string, viewKeyStr: string, outgoingViewKeyStr: string, publicAddressStr: string, intendedTransactionFee: bigint): Buffer + build(proofGenerationKeyStr: string, viewKeyStr: string, outgoingViewKeyStr: string, publicAddressStr: string, intendedTransactionFee: bigint, changeGoesTo?: string | undefined | null): Buffer setExpiration(sequence: number): void } export class FoundBlockResult { diff --git a/ironfish-rust-nodejs/src/structs/transaction.rs b/ironfish-rust-nodejs/src/structs/transaction.rs index 223655aeb0..97d181fe01 100644 --- a/ironfish-rust-nodejs/src/structs/transaction.rs +++ b/ironfish-rust-nodejs/src/structs/transaction.rs @@ -321,6 +321,7 @@ impl NativeTransaction { outgoing_view_key_str: String, public_address_str: String, intended_transaction_fee: BigInt, + change_goes_to: Option, ) -> Result { let view_key = ViewKey::from_hex(&view_key_str).map_err(to_napi_err)?; let outgoing_view_key = @@ -328,6 +329,10 @@ impl NativeTransaction { let public_address = PublicAddress::from_hex(&public_address_str).map_err(to_napi_err)?; let proof_generation_key = ProofGenerationKey::from_hex(&proof_generation_key_str) .map_err(|_| to_napi_err("PublicKeyPackage hex to bytes failed"))?; + let change_address = match change_goes_to { + Some(address) => Some(PublicAddress::from_hex(&address).map_err(to_napi_err)?), + None => None, + }; let unsigned_transaction = self .transaction .build( @@ -336,6 +341,7 @@ impl NativeTransaction { outgoing_view_key, public_address, intended_transaction_fee.get_i64().0, + change_address, ) .map_err(to_napi_err)?; diff --git a/ironfish-rust/src/transaction/mod.rs b/ironfish-rust/src/transaction/mod.rs index 38912549f0..149fb43dde 100644 --- a/ironfish-rust/src/transaction/mod.rs +++ b/ironfish-rust/src/transaction/mod.rs @@ -191,7 +191,7 @@ impl ProposedTransaction { Ok(()) } - pub fn add_change_notes( + fn add_change_notes( &mut self, change_goes_to: Option, public_address: PublicAddress, @@ -223,7 +223,6 @@ impl ProposedTransaction { change_notes.push(change_note); } } - for change_note in change_notes { self.add_output(change_note)?; } @@ -237,7 +236,14 @@ impl ProposedTransaction { outgoing_view_key: OutgoingViewKey, public_address: PublicAddress, intended_transaction_fee: i64, + change_goes_to: Option, ) -> Result { + // skip adding change notes if this is special case of a miners fee transaction + let is_miners_fee = self.outputs.iter().any(|output| output.get_is_miners_fee()); + if !is_miners_fee { + self.add_change_notes(change_goes_to, public_address, intended_transaction_fee)?; + } + // The public key after randomization has been applied. This is used // during signature verification. Referred to as `rk` in the literature // Calculated from the authorizing key and the public_key_randomness. @@ -330,7 +336,6 @@ impl ProposedTransaction { let public_address = spender_key.public_address(); let i64_fee = i64::try_from(intended_transaction_fee)?; - self.add_change_notes(change_goes_to, public_address, i64_fee)?; let unsigned = self.build( spender_key.sapling_proof_generation_key(), @@ -338,6 +343,7 @@ impl ProposedTransaction { spender_key.outgoing_view_key().clone(), public_address, i64_fee, + change_goes_to, )?; unsigned.sign(spender_key) } @@ -378,6 +384,7 @@ impl ProposedTransaction { spender_key.outgoing_view_key().clone(), spender_key.public_address(), *self.value_balances.fee(), + None, )?; unsigned.sign(spender_key) } diff --git a/ironfish-rust/src/transaction/outputs.rs b/ironfish-rust/src/transaction/outputs.rs index 52d7e6aca1..d7f1690277 100644 --- a/ironfish-rust/src/transaction/outputs.rs +++ b/ironfish-rust/src/transaction/outputs.rs @@ -60,6 +60,10 @@ impl OutputBuilder { self.is_miners_fee = true; } + pub(crate) fn get_is_miners_fee(&self) -> bool { + self.is_miners_fee + } + /// Get the value_commitment from this proof as an edwards Point. /// /// This integrates the value and randomness into a single point, using an diff --git a/ironfish-rust/src/transaction/tests.rs b/ironfish-rust/src/transaction/tests.rs index a8f79fa9da..07f42b5eaf 100644 --- a/ironfish-rust/src/transaction/tests.rs +++ b/ironfish-rust/src/transaction/tests.rs @@ -227,11 +227,8 @@ fn test_proposed_transaction_build() { transaction.add_output(out_note).unwrap(); assert_eq!(transaction.outputs.len(), 1); - let public_address = spender_key.public_address(); + let public_address: crate::PublicAddress = spender_key.public_address(); let intended_fee = 1; - transaction - .add_change_notes(Some(public_address), public_address, intended_fee) - .expect("should be able to add change notes"); let unsigned_transaction = transaction .build( @@ -240,6 +237,7 @@ fn test_proposed_transaction_build() { spender_key.outgoing_view_key().clone(), spender_key.public_address(), intended_fee, + Some(public_address), ) .expect("should be able to build unsigned transaction"); From cd199d51966853f5d56d0acf52dcb08f9b745e6a Mon Sep 17 00:00:00 2001 From: Daniel Cogan Date: Mon, 22 Jan 2024 13:51:38 -0800 Subject: [PATCH 32/38] Query the full consensus parameters in the pool and solo miners (#4576) --- ironfish/src/mining/pool.ts | 25 +++++++++++++++++-------- ironfish/src/mining/soloMiner.ts | 17 ++++++++++++++--- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/ironfish/src/mining/pool.ts b/ironfish/src/mining/pool.ts index fc78968b75..15119f19b1 100644 --- a/ironfish/src/mining/pool.ts +++ b/ironfish/src/mining/pool.ts @@ -5,6 +5,7 @@ import { blake3 } from '@napi-rs/blake-hash' import LeastRecentlyUsed from 'blru' import tls from 'tls' import { Assert } from '../assert' +import { ConsensusParameters } from '../consensus' import { Config } from '../fileStores/config' import { Logger } from '../logger' import { Target } from '../primitives/target' @@ -35,6 +36,8 @@ export class MiningPool { readonly config: Config readonly webhooks: WebhookNotifier[] + private consensusParameters: ConsensusParameters | null = null + private started: boolean private stopPromise: Promise | null = null private stopResolve: (() => void) | null = null @@ -351,11 +354,17 @@ export class MiningPool { return } - if (connected) { - const networkResponse = await this.rpc.chain.getNetworkInfo() - const explorer = this.getExplorer(networkResponse.content.networkId) - - this.webhooks.map((w) => w.poolConnected(explorer ?? undefined)) + // Get block explorer URLs based on network ID + const networkResponse = await this.rpc.chain.getNetworkInfo() + const explorer = this.getExplorer(networkResponse.content.networkId) + this.webhooks.map((w) => w.poolConnected(explorer ?? undefined)) + + const consensusResponse = (await this.rpc.chain.getConsensusParameters()).content + this.consensusParameters = { + ...consensusResponse, + enableFishHash: consensusResponse.enableFishHash || 'never', + enableAssetOwnership: consensusResponse.enableAssetOwnership || 'never', + enforceSequentialBlockTime: consensusResponse.enforceSequentialBlockTime || 'never', } this.connectWarned = false @@ -379,13 +388,13 @@ export class MiningPool { } private async processNewBlocks() { - const consensusParameters = (await this.rpc.chain.getConsensusParameters()).content + Assert.isNotNull(this.consensusParameters) for await (const payload of this.rpc.miner.blockTemplateStream().contentStream()) { Assert.isNotUndefined(payload.previousBlockInfo) this.restartCalculateTargetInterval( - consensusParameters.targetBlockTimeInSeconds, - consensusParameters.targetBucketTimeInSeconds, + this.consensusParameters.targetBlockTimeInSeconds, + this.consensusParameters.targetBucketTimeInSeconds, ) const currentHeadTarget = new Target(Buffer.from(payload.previousBlockInfo.target, 'hex')) diff --git a/ironfish/src/mining/soloMiner.ts b/ironfish/src/mining/soloMiner.ts index c2f43520ab..3622a6688f 100644 --- a/ironfish/src/mining/soloMiner.ts +++ b/ironfish/src/mining/soloMiner.ts @@ -4,6 +4,7 @@ import { ThreadPoolHandler } from '@ironfish/rust-nodejs' import { blake3 } from '@napi-rs/blake-hash' import { Assert } from '../assert' +import { ConsensusParameters } from '../consensus' import { Logger } from '../logger' import { Meter } from '../metrics/meter' import { Target } from '../primitives/target' @@ -35,6 +36,8 @@ export class MiningSoloMiner { private miningRequestBlocks: Map private miningRequestId: number + private consensusParameters: ConsensusParameters | null = null + private currentHeadTimestamp: number | null private currentHeadDifficulty: bigint | null @@ -148,7 +151,7 @@ export class MiningSoloMiner { } private async processNewBlocks() { - const consensusParameters = (await this.rpc.chain.getConsensusParameters()).content + Assert.isNotNull(this.consensusParameters) for await (const payload of this.rpc.miner.blockTemplateStream().contentStream()) { Assert.isNotUndefined(payload.previousBlockInfo) @@ -158,8 +161,8 @@ export class MiningSoloMiner { this.currentHeadTimestamp = payload.previousBlockInfo.timestamp this.restartCalculateTargetInterval( - consensusParameters.targetBlockTimeInSeconds, - consensusParameters.targetBucketTimeInSeconds, + this.consensusParameters.targetBlockTimeInSeconds, + this.consensusParameters.targetBucketTimeInSeconds, ) this.startNewWork(payload) } @@ -250,6 +253,14 @@ export class MiningSoloMiner { return } + const consensusResponse = (await this.rpc.chain.getConsensusParameters()).content + this.consensusParameters = { + ...consensusResponse, + enableFishHash: consensusResponse.enableFishHash || 'never', + enableAssetOwnership: consensusResponse.enableAssetOwnership || 'never', + enforceSequentialBlockTime: consensusResponse.enforceSequentialBlockTime || 'never', + } + this.connectWarned = false this.logger.info('Successfully connected to node') this.logger.info('Listening to node for new blocks') From 0031b28d4da68a570e86b00969b067986a6e5e62 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 22 Jan 2024 14:17:46 -0800 Subject: [PATCH 33/38] Rename ERROR_CODES TO RPC_ERROR_CODES (#4578) To keep this consistent with other RPC types. --- ironfish-cli/src/utils/fees.ts | 7 +++++-- ironfish/src/rpc/adapters/errors.ts | 8 ++++---- ironfish/src/rpc/adapters/httpAdapter.ts | 4 ++-- ironfish/src/rpc/adapters/ipcAdapter.test.ts | 6 +++--- .../src/rpc/adapters/socketAdapter/socketAdapter.ts | 6 +++--- ironfish/src/rpc/adapters/tcpAdapter.test.ts | 10 +++++----- ironfish/src/rpc/routes/chain/getBlock.test.ts | 8 ++++---- .../src/rpc/routes/chain/getNetworkHashPower.test.ts | 4 ++-- ironfish/src/rpc/routes/faucet/getFunds.ts | 12 ++++++------ ironfish/src/rpc/routes/router.ts | 4 ++-- ironfish/src/rpc/routes/wallet/create.test.slow.ts | 4 ++-- ironfish/src/rpc/routes/wallet/create.ts | 4 ++-- ironfish/src/rpc/routes/wallet/createAccount.ts | 4 ++-- .../src/rpc/routes/wallet/createTransaction.test.ts | 6 +++--- ironfish/src/rpc/routes/wallet/createTransaction.ts | 4 ++-- ironfish/src/rpc/routes/wallet/rename.test.ts | 4 ++-- ironfish/src/rpc/routes/wallet/renameAccount.test.ts | 4 ++-- .../src/rpc/routes/wallet/sendTransaction.test.ts | 12 ++++++------ ironfish/src/rpc/routes/wallet/sendTransaction.ts | 6 +++--- 19 files changed, 60 insertions(+), 57 deletions(-) diff --git a/ironfish-cli/src/utils/fees.ts b/ironfish-cli/src/utils/fees.ts index 83070ee842..f875196740 100644 --- a/ironfish-cli/src/utils/fees.ts +++ b/ironfish-cli/src/utils/fees.ts @@ -7,10 +7,10 @@ import { Assert, CreateTransactionRequest, CurrencyUtils, - ERROR_CODES, Logger, RawTransaction, RawTransactionSerde, + RPC_ERROR_CODES, RpcClient, RpcRequestError, } from '@ironfish/sdk' @@ -109,7 +109,10 @@ async function getTxWithFee( }) const response = await promise.catch((e) => { - if (e instanceof RpcRequestError && e.code === ERROR_CODES.INSUFFICIENT_BALANCE.valueOf()) { + if ( + e instanceof RpcRequestError && + e.code === RPC_ERROR_CODES.INSUFFICIENT_BALANCE.valueOf() + ) { return null } else { throw e diff --git a/ironfish/src/rpc/adapters/errors.ts b/ironfish/src/rpc/adapters/errors.ts index 39c8e83ff1..9b35167db1 100644 --- a/ironfish/src/rpc/adapters/errors.ts +++ b/ironfish/src/rpc/adapters/errors.ts @@ -3,7 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** All the known error codes for APIs that can be sent back from all APIs */ -export enum ERROR_CODES { +export enum RPC_ERROR_CODES { ACCOUNT_EXISTS = 'account-exists', ERROR = 'error', ROUTE_NOT_FOUND = 'route-not-found', @@ -30,7 +30,7 @@ export class RpcResponseError extends Error { constructor(message: string, code?: string, status?: number) constructor(error: Error, code?: string, status?: number) - constructor(messageOrError: string | Error, code = ERROR_CODES.ERROR, status = 400) { + constructor(messageOrError: string | Error, code = RPC_ERROR_CODES.ERROR, status = 400) { super(messageOrError instanceof Error ? messageOrError.message : messageOrError) if (messageOrError instanceof Error) { @@ -48,7 +48,7 @@ export class RpcResponseError extends Error { * a 400 error to the user based on validation */ export class RpcValidationError extends RpcResponseError { - constructor(message: string, status = 400, code = ERROR_CODES.VALIDATION) { + constructor(message: string, status = 400, code = RPC_ERROR_CODES.VALIDATION) { super(message, code, status) } } @@ -57,7 +57,7 @@ export class RpcValidationError extends RpcResponseError { * A convenience error to throw inside of routes when a resource is not found */ export class RpcNotFoundError extends RpcResponseError { - constructor(message: string, status = 404, code = ERROR_CODES.NOT_FOUND) { + constructor(message: string, status = 404, code = RPC_ERROR_CODES.NOT_FOUND) { super(message, code, status) } } diff --git a/ironfish/src/rpc/adapters/httpAdapter.ts b/ironfish/src/rpc/adapters/httpAdapter.ts index e96aed094f..1980d6ecf0 100644 --- a/ironfish/src/rpc/adapters/httpAdapter.ts +++ b/ironfish/src/rpc/adapters/httpAdapter.ts @@ -11,7 +11,7 @@ import { RpcRequest } from '../request' import { ApiNamespace, Router } from '../routes' import { RpcServer } from '../server' import { IRpcAdapter } from './adapter' -import { ERROR_CODES, RpcResponseError } from './errors' +import { RPC_ERROR_CODES, RpcResponseError } from './errors' import { MESSAGE_DELIMITER } from './socketAdapter' const MEGABYTES = 1000 * 1000 @@ -102,7 +102,7 @@ export class RpcHttpAdapter implements IRpcAdapter { const error = ErrorUtils.renderError(e) this.logger.debug(`Error in HTTP adapter: ${error}`) let errorResponse: RpcHttpError = { - code: ERROR_CODES.ERROR, + code: RPC_ERROR_CODES.ERROR, status: 500, message: error, } diff --git a/ironfish/src/rpc/adapters/ipcAdapter.test.ts b/ironfish/src/rpc/adapters/ipcAdapter.test.ts index 4c4bfaa058..80225b02ad 100644 --- a/ironfish/src/rpc/adapters/ipcAdapter.test.ts +++ b/ironfish/src/rpc/adapters/ipcAdapter.test.ts @@ -9,7 +9,7 @@ import { PromiseUtils } from '../../utils/promise' import { RpcRequestError } from '../clients' import { RpcIpcClient } from '../clients/ipcClient' import { ALL_API_NAMESPACES } from '../routes/router' -import { ERROR_CODES, RpcValidationError } from './errors' +import { RPC_ERROR_CODES, RpcValidationError } from './errors' import { RpcIpcAdapter } from './ipcAdapter' describe('IpcAdapter', () => { @@ -102,7 +102,7 @@ describe('IpcAdapter', () => { it('should handle errors', async () => { ipc.router?.routes.register('foo/bar', yup.object({}), () => { - throw new RpcValidationError('hello error', 402, 'hello-error' as ERROR_CODES) + throw new RpcValidationError('hello error', 402, 'hello-error' as RPC_ERROR_CODES) }) await ipc.start() @@ -134,7 +134,7 @@ describe('IpcAdapter', () => { await expect(response.waitForEnd()).rejects.toThrow(RpcRequestError) await expect(response.waitForEnd()).rejects.toMatchObject({ status: 400, - code: ERROR_CODES.VALIDATION, + code: RPC_ERROR_CODES.VALIDATION, codeMessage: expect.stringContaining('this must be defined'), }) }) diff --git a/ironfish/src/rpc/adapters/socketAdapter/socketAdapter.ts b/ironfish/src/rpc/adapters/socketAdapter/socketAdapter.ts index b13cd19c9f..57919e5325 100644 --- a/ironfish/src/rpc/adapters/socketAdapter/socketAdapter.ts +++ b/ironfish/src/rpc/adapters/socketAdapter/socketAdapter.ts @@ -14,7 +14,7 @@ import { RpcRequest } from '../../request' import { ApiNamespace, Router } from '../../routes' import { RpcServer } from '../../server' import { IRpcAdapter } from '../adapter' -import { ERROR_CODES, RpcResponseError } from '../errors' +import { RPC_ERROR_CODES, RpcResponseError } from '../errors' import { MESSAGE_DELIMITER, RpcSocketClientMessageSchema, @@ -239,7 +239,7 @@ export abstract class RpcSocketAdapter implements IRpcAdapter { const error = message.auth ? 'Failed authentication' : 'Missing authentication token' - throw new RpcResponseError(error, ERROR_CODES.UNAUTHENTICATED, 401) + throw new RpcResponseError(error, RPC_ERROR_CODES.UNAUTHENTICATED, 401) } } @@ -311,7 +311,7 @@ export abstract class RpcSocketAdapter implements IRpcAdapter { const error = new Error(`Malformed request rejected`) const data: RpcSocketError = { - code: ERROR_CODES.ERROR, + code: RPC_ERROR_CODES.ERROR, message: error.message, stack: error.stack, } diff --git a/ironfish/src/rpc/adapters/tcpAdapter.test.ts b/ironfish/src/rpc/adapters/tcpAdapter.test.ts index fb24e9fe91..34a8ccfe52 100644 --- a/ironfish/src/rpc/adapters/tcpAdapter.test.ts +++ b/ironfish/src/rpc/adapters/tcpAdapter.test.ts @@ -13,7 +13,7 @@ import { getUniqueTestDataDir } from '../../testUtilities' import { RpcRequestError } from '../clients' import { RpcTcpClient } from '../clients/tcpClient' import { ALL_API_NAMESPACES } from '../routes' -import { ERROR_CODES, RpcValidationError } from './errors' +import { RPC_ERROR_CODES, RpcValidationError } from './errors' import { RpcTcpAdapter } from './tcpAdapter' describe('TcpAdapter', () => { @@ -97,7 +97,7 @@ describe('TcpAdapter', () => { Assert.isNotNull(tcp?.router) tcp.router.routes.register('foo/bar', yup.object({}), () => { - throw new RpcValidationError('hello error', 402, 'hello-error' as ERROR_CODES) + throw new RpcValidationError('hello error', 402, 'hello-error' as RPC_ERROR_CODES) }) client = new RpcTcpClient('localhost', 0) @@ -132,7 +132,7 @@ describe('TcpAdapter', () => { await expect(response.waitForEnd()).rejects.toThrow(RpcRequestError) await expect(response.waitForEnd()).rejects.toMatchObject({ status: 400, - code: ERROR_CODES.VALIDATION, + code: RPC_ERROR_CODES.VALIDATION, codeMessage: expect.stringContaining('this must be defined'), }) }, 20000) @@ -173,7 +173,7 @@ describe('TcpAdapter', () => { await expect(response.waitForEnd()).rejects.toMatchObject({ status: 401, - code: ERROR_CODES.UNAUTHENTICATED, + code: RPC_ERROR_CODES.UNAUTHENTICATED, codeMessage: expect.stringContaining('Failed authentication'), }) }, 20000) @@ -197,7 +197,7 @@ describe('TcpAdapter', () => { await expect(response.waitForEnd()).rejects.toMatchObject({ status: 401, - code: ERROR_CODES.UNAUTHENTICATED, + code: RPC_ERROR_CODES.UNAUTHENTICATED, codeMessage: expect.stringContaining('Missing authentication token'), }) }, 20000) diff --git a/ironfish/src/rpc/routes/chain/getBlock.test.ts b/ironfish/src/rpc/routes/chain/getBlock.test.ts index e4916c017d..3f80fb9b63 100644 --- a/ironfish/src/rpc/routes/chain/getBlock.test.ts +++ b/ironfish/src/rpc/routes/chain/getBlock.test.ts @@ -4,7 +4,7 @@ import { Assert } from '../../../assert' import { useBlockWithTx, useMinerBlockFixture } from '../../../testUtilities' import { createRouteTest } from '../../../testUtilities/routeTest' -import { ERROR_CODES } from '../../adapters' +import { RPC_ERROR_CODES } from '../../adapters' import { RpcRequestError } from '../../clients/errors' import { GetBlockResponse } from './getBlock' @@ -50,7 +50,7 @@ describe('Route chain/getBlock', () => { throw e } expect(e.status).toBe(404) - expect(e.code).toBe(ERROR_CODES.NOT_FOUND) + expect(e.code).toBe(RPC_ERROR_CODES.NOT_FOUND) expect(e.message).toContain('No block found with hash') } @@ -88,7 +88,7 @@ describe('Route chain/getBlock', () => { throw e } expect(e.status).toBe(404) - expect(e.code).toBe(ERROR_CODES.NOT_FOUND) + expect(e.code).toBe(RPC_ERROR_CODES.NOT_FOUND) expect(e.message).toContain('No block found with sequence') } @@ -104,7 +104,7 @@ describe('Route chain/getBlock', () => { throw e } expect(e.status).toBe(404) - expect(e.code).toBe(ERROR_CODES.NOT_FOUND) + expect(e.code).toBe(RPC_ERROR_CODES.NOT_FOUND) expect(e.message).toContain('No block with header') } }) diff --git a/ironfish/src/rpc/routes/chain/getNetworkHashPower.test.ts b/ironfish/src/rpc/routes/chain/getNetworkHashPower.test.ts index 0bdd137e89..bfded7486c 100644 --- a/ironfish/src/rpc/routes/chain/getNetworkHashPower.test.ts +++ b/ironfish/src/rpc/routes/chain/getNetworkHashPower.test.ts @@ -4,7 +4,7 @@ import { useAccountFixture, useMinerBlockFixture } from '../../../testUtilities/fixtures' import { createRouteTest } from '../../../testUtilities/routeTest' import { Account } from '../../../wallet' -import { ERROR_CODES } from '../../adapters' +import { RPC_ERROR_CODES } from '../../adapters' describe('Route chain/getNetworkHashPower', () => { const routeTest = createRouteTest(true) @@ -76,7 +76,7 @@ describe('Route chain/getNetworkHashPower', () => { expect.objectContaining({ message: expect.stringContaining('[blocks] value must be greater than 0'), status: 400, - code: ERROR_CODES.VALIDATION, + code: RPC_ERROR_CODES.VALIDATION, }), ) }) diff --git a/ironfish/src/rpc/routes/faucet/getFunds.ts b/ironfish/src/rpc/routes/faucet/getFunds.ts index fc0f40ffd3..b83f92f01e 100644 --- a/ironfish/src/rpc/routes/faucet/getFunds.ts +++ b/ironfish/src/rpc/routes/faucet/getFunds.ts @@ -4,7 +4,7 @@ import axios, { AxiosError } from 'axios' import * as yup from 'yup' import { Assert } from '../../../assert' -import { ERROR_CODES, RpcResponseError } from '../../adapters' +import { RPC_ERROR_CODES, RpcResponseError } from '../../adapters' import { ApiNamespace } from '../namespaces' import { routes } from '../router' import { AssertHasRpcContext } from '../rpcContext' @@ -39,7 +39,7 @@ routes.register( // not testnet throw new RpcResponseError( 'This endpoint is only available for testnet.', - ERROR_CODES.ERROR, + RPC_ERROR_CODES.ERROR, ) } @@ -57,20 +57,20 @@ routes.register( if (status === 422) { if (data.code === 'faucet_max_requests_reached') { Assert.isNotUndefined(data.message) - throw new RpcResponseError(data.message, ERROR_CODES.VALIDATION, status) + throw new RpcResponseError(data.message, RPC_ERROR_CODES.VALIDATION, status) } throw new RpcResponseError( 'You entered an invalid email.', - ERROR_CODES.VALIDATION, + RPC_ERROR_CODES.VALIDATION, status, ) } else if (data.message) { - throw new RpcResponseError(data.message, ERROR_CODES.ERROR, status) + throw new RpcResponseError(data.message, RPC_ERROR_CODES.ERROR, status) } } - throw new RpcResponseError(error.message, ERROR_CODES.ERROR, Number(error.code)) + throw new RpcResponseError(error.message, RPC_ERROR_CODES.ERROR, Number(error.code)) }) request.end({ diff --git a/ironfish/src/rpc/routes/router.ts b/ironfish/src/rpc/routes/router.ts index aaf93a359f..04fee90445 100644 --- a/ironfish/src/rpc/routes/router.ts +++ b/ironfish/src/rpc/routes/router.ts @@ -4,7 +4,7 @@ import { Assert } from '../../assert' import { YupSchema, YupSchemaResult, YupUtils } from '../../utils' import { StrEnumUtils } from '../../utils/enums' -import { ERROR_CODES } from '../adapters' +import { RPC_ERROR_CODES } from '../adapters' import { RpcResponseError, RpcValidationError } from '../adapters/errors' import { RpcRequest } from '../request' import { RpcServer } from '../server' @@ -22,7 +22,7 @@ export class RouteNotFoundError extends RpcResponseError { constructor(route: string, namespace: string, method: string) { super( `No route found ${route} in namespace ${namespace} for method ${method}`, - ERROR_CODES.ROUTE_NOT_FOUND, + RPC_ERROR_CODES.ROUTE_NOT_FOUND, 404, ) } diff --git a/ironfish/src/rpc/routes/wallet/create.test.slow.ts b/ironfish/src/rpc/routes/wallet/create.test.slow.ts index e58a6bd11f..930db55a87 100644 --- a/ironfish/src/rpc/routes/wallet/create.test.slow.ts +++ b/ironfish/src/rpc/routes/wallet/create.test.slow.ts @@ -7,7 +7,7 @@ import { v4 as uuid } from 'uuid' import { createRouteTest } from '../../../testUtilities/routeTest' -import { ERROR_CODES } from '../../adapters' +import { RPC_ERROR_CODES } from '../../adapters' import { RpcRequestError } from '../../clients/errors' describe('Route wallet/create', () => { @@ -65,7 +65,7 @@ describe('Route wallet/create', () => { throw e } expect(e.status).toBe(400) - expect(e.code).toBe(ERROR_CODES.ACCOUNT_EXISTS) + expect(e.code).toBe(RPC_ERROR_CODES.ACCOUNT_EXISTS) } }) diff --git a/ironfish/src/rpc/routes/wallet/create.ts b/ironfish/src/rpc/routes/wallet/create.ts index ff780555b5..4c9a9eaf7d 100644 --- a/ironfish/src/rpc/routes/wallet/create.ts +++ b/ironfish/src/rpc/routes/wallet/create.ts @@ -8,7 +8,7 @@ * is the verbObject naming convention. For example, `POST /wallet/burnAsset` burns an asset. */ -import { ERROR_CODES, RpcValidationError } from '../../adapters' +import { RPC_ERROR_CODES, RpcValidationError } from '../../adapters' import { ApiNamespace } from '../namespaces' import { routes } from '../router' import { AssertHasRpcContext } from '../rpcContext' @@ -26,7 +26,7 @@ routes.register( throw new RpcValidationError( `There is already an account with the name ${name}`, 400, - ERROR_CODES.ACCOUNT_EXISTS, + RPC_ERROR_CODES.ACCOUNT_EXISTS, ) } diff --git a/ironfish/src/rpc/routes/wallet/createAccount.ts b/ironfish/src/rpc/routes/wallet/createAccount.ts index 5ec0cf76c0..bd2708f931 100644 --- a/ironfish/src/rpc/routes/wallet/createAccount.ts +++ b/ironfish/src/rpc/routes/wallet/createAccount.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 * as yup from 'yup' -import { ERROR_CODES, RpcValidationError } from '../../adapters' +import { RPC_ERROR_CODES, RpcValidationError } from '../../adapters' import { ApiNamespace } from '../namespaces' import { routes } from '../router' import { AssertHasRpcContext } from '../rpcContext' @@ -51,7 +51,7 @@ routes.register( throw new RpcValidationError( `There is already an account with the name ${name}`, 400, - ERROR_CODES.ACCOUNT_EXISTS, + RPC_ERROR_CODES.ACCOUNT_EXISTS, ) } diff --git a/ironfish/src/rpc/routes/wallet/createTransaction.test.ts b/ironfish/src/rpc/routes/wallet/createTransaction.test.ts index 6e007a87a0..aaebd670c3 100644 --- a/ironfish/src/rpc/routes/wallet/createTransaction.test.ts +++ b/ironfish/src/rpc/routes/wallet/createTransaction.test.ts @@ -7,7 +7,7 @@ import { RawTransactionSerde } from '../../../primitives/rawTransaction' import { useAccountFixture, useMinerBlockFixture } from '../../../testUtilities' import { createRouteTest } from '../../../testUtilities/routeTest' import { AsyncUtils } from '../../../utils' -import { ERROR_CODES } from '../../adapters/errors' +import { RPC_ERROR_CODES } from '../../adapters/errors' const REQUEST_PARAMS = { account: 'existingAccount', @@ -51,7 +51,7 @@ describe('Route wallet/createTransaction', () => { expect.objectContaining({ message: expect.any(String), status: 400, - code: ERROR_CODES.INSUFFICIENT_BALANCE, + code: RPC_ERROR_CODES.INSUFFICIENT_BALANCE, }), ) }) @@ -380,7 +380,7 @@ describe('Route wallet/createTransaction', () => { expect.objectContaining({ message: expect.any(String), status: 400, - code: ERROR_CODES.INSUFFICIENT_BALANCE, + code: RPC_ERROR_CODES.INSUFFICIENT_BALANCE, }), ) }) diff --git a/ironfish/src/rpc/routes/wallet/createTransaction.ts b/ironfish/src/rpc/routes/wallet/createTransaction.ts index 0e78b082ed..10f5141b3c 100644 --- a/ironfish/src/rpc/routes/wallet/createTransaction.ts +++ b/ironfish/src/rpc/routes/wallet/createTransaction.ts @@ -13,7 +13,7 @@ import { RawTransactionSerde } from '../../../primitives/rawTransaction' import { CurrencyUtils, YupUtils } from '../../../utils' import { Wallet } from '../../../wallet' import { NotEnoughFundsError } from '../../../wallet/errors' -import { ERROR_CODES, RpcValidationError } from '../../adapters/errors' +import { RPC_ERROR_CODES, RpcValidationError } from '../../adapters/errors' import { ApiNamespace } from '../namespaces' import { routes } from '../router' import { AssertHasRpcContext } from '../rpcContext' @@ -209,7 +209,7 @@ routes.register { expect.objectContaining({ message: expect.any(String), status: 400, - code: ERROR_CODES.VALIDATION, + code: RPC_ERROR_CODES.VALIDATION, }), ) }) diff --git a/ironfish/src/rpc/routes/wallet/renameAccount.test.ts b/ironfish/src/rpc/routes/wallet/renameAccount.test.ts index 6cd1598df7..1a1e560256 100644 --- a/ironfish/src/rpc/routes/wallet/renameAccount.test.ts +++ b/ironfish/src/rpc/routes/wallet/renameAccount.test.ts @@ -4,7 +4,7 @@ import { useAccountFixture } from '../../../testUtilities' import { createRouteTest } from '../../../testUtilities/routeTest' -import { ERROR_CODES } from '../../adapters/errors' +import { RPC_ERROR_CODES } from '../../adapters/errors' const REQUEST_PARAMS = { account: 'existingAccount', @@ -19,7 +19,7 @@ describe('Route wallet/renameAccount', () => { expect.objectContaining({ message: expect.any(String), status: 400, - code: ERROR_CODES.VALIDATION, + code: RPC_ERROR_CODES.VALIDATION, }), ) }) diff --git a/ironfish/src/rpc/routes/wallet/sendTransaction.test.ts b/ironfish/src/rpc/routes/wallet/sendTransaction.test.ts index 8304334e18..981b9d2f01 100644 --- a/ironfish/src/rpc/routes/wallet/sendTransaction.test.ts +++ b/ironfish/src/rpc/routes/wallet/sendTransaction.test.ts @@ -7,7 +7,7 @@ import { Assert } from '../../../assert' import { useAccountFixture, useMinersTxFixture } from '../../../testUtilities/fixtures' import { createRouteTest } from '../../../testUtilities/routeTest' import { NotEnoughFundsError } from '../../../wallet/errors' -import { ERROR_CODES } from '../../adapters' +import { RPC_ERROR_CODES } from '../../adapters' const TEST_PARAMS = { account: 'existingAccount', @@ -74,7 +74,7 @@ describe('Route wallet/sendTransaction', () => { `Your balance is too low. Add funds to your account first`, ), status: 400, - code: ERROR_CODES.INSUFFICIENT_BALANCE, + code: RPC_ERROR_CODES.INSUFFICIENT_BALANCE, }), ) @@ -84,7 +84,7 @@ describe('Route wallet/sendTransaction', () => { `Your balance is too low. Add funds to your account first`, ), status: 400, - code: ERROR_CODES.INSUFFICIENT_BALANCE, + code: RPC_ERROR_CODES.INSUFFICIENT_BALANCE, }), ) }) @@ -109,7 +109,7 @@ describe('Route wallet/sendTransaction', () => { `Your balance is too low. Add funds to your account first`, ), status: 400, - code: ERROR_CODES.INSUFFICIENT_BALANCE, + code: RPC_ERROR_CODES.INSUFFICIENT_BALANCE, }), ) @@ -130,7 +130,7 @@ describe('Route wallet/sendTransaction', () => { `Your balance is too low. Add funds to your account first`, ), status: 400, - code: ERROR_CODES.INSUFFICIENT_BALANCE, + code: RPC_ERROR_CODES.INSUFFICIENT_BALANCE, }), ) }) @@ -157,7 +157,7 @@ describe('Route wallet/sendTransaction', () => { await expect(routeTest.client.wallet.sendTransaction(TEST_PARAMS)).rejects.toThrow( expect.objectContaining({ status: 400, - code: ERROR_CODES.INSUFFICIENT_BALANCE, + code: RPC_ERROR_CODES.INSUFFICIENT_BALANCE, }), ) }) diff --git a/ironfish/src/rpc/routes/wallet/sendTransaction.ts b/ironfish/src/rpc/routes/wallet/sendTransaction.ts index ba9616c1ee..985b057e04 100644 --- a/ironfish/src/rpc/routes/wallet/sendTransaction.ts +++ b/ironfish/src/rpc/routes/wallet/sendTransaction.ts @@ -8,7 +8,7 @@ import { Assert } from '../../../assert' import { CurrencyUtils, YupUtils } from '../../../utils' import { Wallet } from '../../../wallet' import { NotEnoughFundsError } from '../../../wallet/errors' -import { ERROR_CODES, RpcValidationError } from '../../adapters/errors' +import { RPC_ERROR_CODES, RpcValidationError } from '../../adapters/errors' import { ApiNamespace } from '../namespaces' import { routes } from '../router' import { AssertHasRpcContext } from '../rpcContext' @@ -126,7 +126,7 @@ routes.register( throw new RpcValidationError( `Your balance is too low. Add funds to your account first`, undefined, - ERROR_CODES.INSUFFICIENT_BALANCE, + RPC_ERROR_CODES.INSUFFICIENT_BALANCE, ) } } @@ -145,7 +145,7 @@ routes.register( }) } catch (e) { if (e instanceof NotEnoughFundsError) { - throw new RpcValidationError(e.message, 400, ERROR_CODES.INSUFFICIENT_BALANCE) + throw new RpcValidationError(e.message, 400, RPC_ERROR_CODES.INSUFFICIENT_BALANCE) } throw e } From 7447f3861ade78a1ff72f3a15608b59d931508a1 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 22 Jan 2024 14:33:51 -0800 Subject: [PATCH 34/38] Fixed typo of delimiter (#4579) This fixes a typo pointed out in another PR --- ironfish/src/rpc/adapters/httpAdapter.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ironfish/src/rpc/adapters/httpAdapter.ts b/ironfish/src/rpc/adapters/httpAdapter.ts index 1980d6ecf0..1b24da7583 100644 --- a/ironfish/src/rpc/adapters/httpAdapter.ts +++ b/ironfish/src/rpc/adapters/httpAdapter.ts @@ -212,14 +212,14 @@ export class RpcHttpAdapter implements IRpcAdapter { route, (status: number, data?: unknown) => { response.statusCode = status - const delimeter = chunkStreamed ? MESSAGE_DELIMITER : '' + const delimiter = chunkStreamed ? MESSAGE_DELIMITER : '' const responseData = JSON.stringify({ status, data }) const responseSize = Buffer.byteLength(responseData, 'utf-8') this.outboundTraffic.add(responseSize) this.outboundBytes.value += responseSize - response.end(delimeter + responseData) + response.end(delimiter + responseData) this.cleanUpRequest(requestId) }, @@ -228,14 +228,14 @@ export class RpcHttpAdapter implements IRpcAdapter { // they wait until all chunks have been received and combine them. This will // stream a delimitated list of JSON objects but is still probably not // ideal as a response. We could find some better way to stream - const delimeter = chunkStreamed ? MESSAGE_DELIMITER : '' + const delimiter = chunkStreamed ? MESSAGE_DELIMITER : '' const responseData = JSON.stringify({ data }) const responseSize = Buffer.byteLength(responseData, 'utf-8') this.outboundTraffic.add(responseSize) this.outboundBytes.value += responseSize - response.write(delimeter + responseData) + response.write(delimiter + responseData) chunkStreamed = true }, ) From 426ccc7e558fb096d6d184be35b5b51b020479c9 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 22 Jan 2024 15:07:25 -0800 Subject: [PATCH 35/38] Add RpcHttpClient (#4571) this adds a new RpcHttpClient to the SDK which allows you to make HTTP requests against the RpcHttpAdapter. --- ironfish/src/rpc/adapters/httpAdapter.test.ts | 117 ++++++++ ironfish/src/rpc/adapters/httpAdapter.ts | 250 ++++++++++-------- ironfish/src/rpc/clients/httpClient.ts | 146 ++++++++++ ironfish/src/rpc/clients/index.ts | 1 + 4 files changed, 409 insertions(+), 105 deletions(-) create mode 100644 ironfish/src/rpc/adapters/httpAdapter.test.ts create mode 100644 ironfish/src/rpc/clients/httpClient.ts diff --git a/ironfish/src/rpc/adapters/httpAdapter.test.ts b/ironfish/src/rpc/adapters/httpAdapter.test.ts new file mode 100644 index 0000000000..af34c35c00 --- /dev/null +++ b/ironfish/src/rpc/adapters/httpAdapter.test.ts @@ -0,0 +1,117 @@ +/* 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/. */ +/* eslint-disable jest/no-conditional-expect */ +import Mitm from 'mitm' +import * as yup from 'yup' +import { Assert } from '../../assert' +import { createNodeTest } from '../../testUtilities' +import { RpcHttpClient, RpcRequestError } from '../clients' +import { ALL_API_NAMESPACES } from '../routes' +import { RPC_ERROR_CODES, RpcValidationError } from './errors' +import { RpcHttpAdapter } from './httpAdapter' + +describe('HttpAdapter', () => { + let httpAdapter: RpcHttpAdapter | undefined + let mitm: ReturnType + + const nodeTest = createNodeTest(false, { + config: { + enableRpc: false, + enableRpcIpc: false, + enableRpcTcp: false, + enableRpcTls: false, + rpcHttpPort: 0, + }, + }) + + beforeEach(async () => { + httpAdapter = new RpcHttpAdapter('localhost', 0, undefined, ALL_API_NAMESPACES) + + mitm = Mitm() + mitm.on('request', (req, res) => httpAdapter?.onRequest(req, res)) + + await nodeTest.node.rpc.mount(httpAdapter) + }, 20000) + + afterEach(() => { + mitm.disable() + }) + + it('should send and receive message', async () => { + Assert.isNotUndefined(httpAdapter) + Assert.isNotNull(httpAdapter.router) + + httpAdapter.router.routes.register('foo/bar', yup.string(), (request) => { + request.end(request.data) + }) + + const client = new RpcHttpClient('http://localhost') + + const response = await client.request('foo/bar', 'hello world').waitForEnd() + expect(response.content).toBe('hello world') + }, 20000) + + it('should stream message', async () => { + Assert.isNotUndefined(httpAdapter) + Assert.isNotNull(httpAdapter?.router) + + httpAdapter.router.routes.register('foo/bar', yup.object({}), (request) => { + request.stream('hello 1') + request.stream('hello 2') + request.end() + }) + + const client = new RpcHttpClient('http://localhost') + + const response = client.request('foo/bar') + expect((await response.contentStream().next()).value).toBe('hello 1') + expect((await response.contentStream().next()).value).toBe('hello 2') + + await response.waitForEnd() + expect(response.content).toBe(undefined) + }, 20000) + + it('should handle errors', async () => { + Assert.isNotUndefined(httpAdapter) + Assert.isNotNull(httpAdapter?.router) + + httpAdapter.router.routes.register('foo/bar', yup.object({}), () => { + throw new RpcValidationError('hello error', 402, 'hello-error' as RPC_ERROR_CODES) + }) + + const client = new RpcHttpClient('http://localhost') + + const response = client.request('foo/bar') + + await expect(response.waitForEnd()).rejects.toThrow(RpcRequestError) + await expect(response.waitForEnd()).rejects.toMatchObject({ + status: 402, + code: 'hello-error', + codeMessage: 'hello error', + }) + }, 20000) + + it('should handle request errors', async () => { + Assert.isNotUndefined(httpAdapter) + Assert.isNotNull(httpAdapter?.router) + + // Requires this + const schema = yup.string().defined() + // But send this instead + const body = undefined + + httpAdapter.router.routes.register('foo/bar', schema, (res) => res.end()) + + const client = new RpcHttpClient('http://localhost') + + const response = client.request('foo/bar', body) + + await expect(response.waitForEnd()).rejects.toThrow(RpcRequestError) + await expect(response.waitForEnd()).rejects.toMatchObject({ + status: 400, + code: RPC_ERROR_CODES.VALIDATION, + codeMessage: expect.stringContaining('this must be defined'), + }) + }, 20000) +}) diff --git a/ironfish/src/rpc/adapters/httpAdapter.ts b/ironfish/src/rpc/adapters/httpAdapter.ts index 1b24da7583..fa392629cb 100644 --- a/ironfish/src/rpc/adapters/httpAdapter.ts +++ b/ironfish/src/rpc/adapters/httpAdapter.ts @@ -3,6 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import http from 'http' import { v4 as uuid } from 'uuid' +import * as yup from 'yup' import { Assert } from '../../assert' import { createRootLogger, Logger } from '../../logger' import { Gauge, Meter } from '../../metrics' @@ -24,6 +25,27 @@ export type RpcHttpError = { stack?: string } +export const RpcHttpErrorSchema: yup.ObjectSchema = yup + .object({ + status: yup.number().defined(), + code: yup.string().defined(), + message: yup.string().defined(), + stack: yup.string().optional(), + }) + .required() + +export type RpcHttpResponse = { + status?: number + data: unknown +} + +export const RpcHttpResponseSchema: yup.ObjectSchema = yup + .object({ + status: yup.number().optional(), + data: yup.mixed().optional(), + }) + .required() + export class RpcHttpAdapter implements IRpcAdapter { server: http.Server | null = null router: Router | null = null @@ -84,46 +106,9 @@ export class RpcHttpAdapter implements IRpcAdapter { server.off('error', onError) server.off('listening', onListening) - server.on('request', (req, res) => { - const requestId = uuid() - const waitForClose = new Promise((resolve) => { - res.on('close', () => { - this.cleanUpRequest(requestId) - resolve() - }) - }) - - this.requests.set(requestId, { req, waitForClose }) - - // All response bodies should be application/json - res.setHeader('Content-Type', 'application/json') - - void this.handleRequest(req, res, requestId).catch((e) => { - const error = ErrorUtils.renderError(e) - this.logger.debug(`Error in HTTP adapter: ${error}`) - let errorResponse: RpcHttpError = { - code: RPC_ERROR_CODES.ERROR, - status: 500, - message: error, - } - - if (e instanceof RpcResponseError) { - errorResponse = { - code: e.code, - status: e.status, - message: e.message, - stack: e.stack, - } - } - - res.writeHead(errorResponse.status) - res.end(JSON.stringify(errorResponse)) - - this.cleanUpRequest(requestId) - }) + server.on('request', (req: http.IncomingMessage, res: http.ServerResponse) => { + this.onRequest(req, res) }) - - resolve() } server.on('error', onError) @@ -132,6 +117,24 @@ export class RpcHttpAdapter implements IRpcAdapter { }) } + onRequest(req: http.IncomingMessage, res: http.ServerResponse): void { + const requestId = uuid() + + const waitForClose = new Promise((resolve) => { + res.on('close', () => { + this.cleanUpRequest(requestId) + resolve() + }) + }) + + this.requests.set(requestId, { req, waitForClose }) + + // All response bodies should be application/json + res.setHeader('Content-Type', 'application/json') + + void this.handleRequest(req, res, requestId) + } + async stop(): Promise { for (const { req, rpcRequest } of this.requests.values()) { req.destroy() @@ -164,86 +167,123 @@ export class RpcHttpAdapter implements IRpcAdapter { response: http.ServerResponse, requestId: string, ): Promise { - if (this.router === null || this.router.server === null) { - throw new RpcResponseError('Tried to connect to unmounted adapter') - } - - const router = this.router - - if (request.url === undefined) { - throw new RpcResponseError('No request url provided') - } + let chunkStreamed = false - this.logger.debug( - `Call HTTP RPC: ${request.method || 'undefined'} ${request.url || 'undefined'}`, - ) + try { + if (this.router === null || this.router.server === null) { + throw new RpcResponseError('Tried to connect to unmounted adapter') + } - const route = this.formatRoute(request) - if (route === undefined) { - throw new RpcResponseError('No route found') - } + const router = this.router - // TODO(daniel): clean up reading body code here a bit of possible - let size = 0 - const data: Buffer[] = [] + if (request.url === undefined) { + throw new RpcResponseError('No request url provided') + } - for await (const chunk of request) { - Assert.isInstanceOf(chunk, Buffer) - size += chunk.byteLength - data.push(chunk) + this.logger.debug( + `Call HTTP RPC: ${request.method || 'undefined'} ${request.url || 'undefined'}`, + ) - if (size >= MAX_REQUEST_SIZE) { - throw new RpcResponseError('Max request size exceeded') + const route = this.formatRoute(request) + if (route === undefined) { + throw new RpcResponseError('No route found') } - } - - const combined = Buffer.concat(data) - this.inboundTraffic.add(size) - this.inboundBytes.value += size + // TODO(daniel): clean up reading body code here a bit of possible + let size = 0 + const data: Buffer[] = [] - // TODO(daniel): some routes assume that no data will be passed as undefined - // so keeping that convention here. Could think of a better way to handle? - const body = combined.length ? combined.toString('utf8') : undefined + for await (const chunk of request) { + Assert.isInstanceOf(chunk, Buffer) + size += chunk.byteLength + data.push(chunk) - let chunkStreamed = false - const rpcRequest = new RpcRequest( - body === undefined ? undefined : JSON.parse(body), - route, - (status: number, data?: unknown) => { - response.statusCode = status - const delimiter = chunkStreamed ? MESSAGE_DELIMITER : '' + if (size >= MAX_REQUEST_SIZE) { + throw new RpcResponseError('Max request size exceeded') + } + } - const responseData = JSON.stringify({ status, data }) - const responseSize = Buffer.byteLength(responseData, 'utf-8') - this.outboundTraffic.add(responseSize) - this.outboundBytes.value += responseSize + const combined = Buffer.concat(data) + + this.inboundTraffic.add(size) + this.inboundBytes.value += size + + // TODO(daniel): some routes assume that no data will be passed as undefined + // so keeping that convention here. Could think of a better way to handle? + const body = combined.length ? combined.toString('utf8') : undefined + + const rpcRequest = new RpcRequest( + body === undefined ? undefined : JSON.parse(body), + route, + (status: number, data?: unknown) => { + response.statusCode = status + const delimiter = chunkStreamed ? MESSAGE_DELIMITER : '' + + const responseMessage: RpcHttpResponse = { status, data } + const responseData = JSON.stringify(responseMessage) + const responseSize = Buffer.byteLength(responseData, 'utf-8') + this.outboundTraffic.add(responseSize) + this.outboundBytes.value += responseSize + + response.end(delimiter + responseData) + + this.cleanUpRequest(requestId) + }, + (data: unknown) => { + // TODO: Most HTTP clients don't parse `Transfer-Encoding: chunked` by chunk + // they wait until all chunks have been received and combine them. This will + // stream a delimitated list of JSON objects but is still probably not + // ideal as a response. We could find some better way to stream + const delimiter = chunkStreamed ? MESSAGE_DELIMITER : '' + + const responseData = JSON.stringify({ data }) + const responseSize = Buffer.byteLength(responseData, 'utf-8') + this.outboundTraffic.add(responseSize) + this.outboundBytes.value += responseSize + + response.write(delimiter + responseData) + chunkStreamed = true + }, + ) + + const currRequest = this.requests.get(requestId) + currRequest && this.requests.set(requestId, { ...currRequest, rpcRequest }) + + await router.route(route, rpcRequest) + } catch (e) { + const error = ErrorUtils.renderError(e) + this.logger.debug(`Error in HTTP adapter: ${error}`) + + const responseMessage: RpcHttpError = + e instanceof RpcResponseError + ? { + code: e.code, + status: e.status, + message: e.message, + stack: e.stack, + } + : { + code: RPC_ERROR_CODES.ERROR, + status: 500, + message: error, + } - response.end(delimiter + responseData) + // If we sent a streaming response we cannot send + // headers again with the status + if (!response.headersSent) { + response.writeHead(responseMessage.status) + } - this.cleanUpRequest(requestId) - }, - (data: unknown) => { - // TODO: Most HTTP clients don't parse `Transfer-Encoding: chunked` by chunk - // they wait until all chunks have been received and combine them. This will - // stream a delimitated list of JSON objects but is still probably not - // ideal as a response. We could find some better way to stream - const delimiter = chunkStreamed ? MESSAGE_DELIMITER : '' - - const responseData = JSON.stringify({ data }) - const responseSize = Buffer.byteLength(responseData, 'utf-8') - this.outboundTraffic.add(responseSize) - this.outboundBytes.value += responseSize - - response.write(delimiter + responseData) - chunkStreamed = true - }, - ) + const delimiter = chunkStreamed ? MESSAGE_DELIMITER : '' - const currRequest = this.requests.get(requestId) - currRequest && this.requests.set(requestId, { ...currRequest, rpcRequest }) + const responseData = JSON.stringify(responseMessage) + const responseSize = Buffer.byteLength(responseData, 'utf-8') + this.outboundTraffic.add(responseSize) + this.outboundBytes.value += responseSize - await router.route(route, rpcRequest) + response.end(delimiter + responseData) + this.cleanUpRequest(requestId) + } } // TODO(daniel): better way to parse method from request here diff --git a/ironfish/src/rpc/clients/httpClient.ts b/ironfish/src/rpc/clients/httpClient.ts new file mode 100644 index 0000000000..442b8c46ec --- /dev/null +++ b/ironfish/src/rpc/clients/httpClient.ts @@ -0,0 +1,146 @@ +/* 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 Axios, { AxiosInstance } from 'axios' +import http from 'http' +import * as yup from 'yup' +import { ErrorUtils, PromiseUtils, YupUtils } from '../../utils' +import { RpcHttpErrorSchema, RpcHttpResponseSchema } from '../adapters/httpAdapter' +import { RpcClient } from '../clients/client' +import { MessageBuffer } from '../messageBuffer' +import { isRpcResponseError, RpcResponse } from '../response' +import { Stream } from '../stream' +import { RpcConnectionRefusedError, RpcRequestError, RpcRequestTimeoutError } from './errors' + +export const RpcHttpMessageSchema = yup + .object({ + status: yup.number().optional(), + }) + .required() + +export class RpcHttpClient extends RpcClient { + protected readonly axios: AxiosInstance + + constructor(baseURL: string) { + super() + this.axios = Axios.create({ baseURL }) + } + + request( + route: string, + data?: unknown, + options: { + timeoutMs?: number | null + } = {}, + ): RpcResponse { + const timeoutMs = options.timeoutMs ?? 0 + const messageBuffer = new MessageBuffer('\f') + const [promise, resolve, reject] = PromiseUtils.split() + const stream = new Stream() + const rpcResponse = new RpcResponse(promise, stream) + + const onData = async (data: Buffer): Promise => { + messageBuffer.write(data) + + for (const message of messageBuffer.readMessages()) { + const parsed: unknown = JSON.parse(message) + + const { result, error } = await YupUtils.tryValidate(RpcHttpMessageSchema, parsed) + + if (!result) { + throw error + } + + if (result.status) { + rpcResponse.status = result.status + } + + if (isRpcResponseError(rpcResponse as RpcResponse)) { + const { result: errorBody, error: errorError } = await YupUtils.tryValidate( + RpcHttpErrorSchema, + parsed, + ) + + if (errorBody) { + const err = new RpcRequestError( + rpcResponse, + errorBody.code, + errorBody.message, + errorBody.stack, + ) + stream.close(err) + reject(err) + } else if (errorError) { + stream.close(errorError) + reject(errorError) + } else { + stream.close(data) + reject(data) + } + return + } + + const { result: messageBody, error: messageError } = await YupUtils.tryValidate( + RpcHttpResponseSchema, + parsed, + ) + + if (messageError) { + throw messageError + } + + if (result.status !== undefined) { + stream.close() + resolve(messageBody.data as TEnd) + return + } + + stream.write(messageBody.data as TStream) + } + } + + const body = JSON.stringify(data) + + void this.axios + .post(route, body, { + responseType: 'stream', + timeout: timeoutMs, + validateStatus: () => true, + transitional: { + clarifyTimeoutError: true, + forcedJSONParsing: true, + silentJSONParsing: true, + }, + }) + .then((response) => { + response.data.on('data', (data: Buffer) => { + void onData(data) + }) + + response.data.on('end', () => { + void onData(Buffer.from('\f')) + }) + }) + .catch((error) => { + if (ErrorUtils.isConnectTimeOutError(error)) { + const errorTimeout = new RpcRequestTimeoutError(rpcResponse, timeoutMs, route) + stream.close(errorTimeout) + reject(errorTimeout) + return + } + + if (ErrorUtils.isConnectRefusedError(error)) { + const errorRefused = new RpcConnectionRefusedError(`Failed to connect to ${route}`) + stream.close(errorRefused) + reject(errorRefused) + return + } + + stream.close(error) + reject(error) + }) + + return rpcResponse + } +} diff --git a/ironfish/src/rpc/clients/index.ts b/ironfish/src/rpc/clients/index.ts index ef76b8572f..9aaf7c2694 100644 --- a/ironfish/src/rpc/clients/index.ts +++ b/ironfish/src/rpc/clients/index.ts @@ -3,6 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ export * from './errors' export * from './client' +export * from './httpClient' export * from './memoryClient' export * from './socketClient' export * from './tcpClient' From 396937396d0ff2287f4dad2ac53eac3f92bcb45d Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Mon, 22 Jan 2024 15:09:41 -0800 Subject: [PATCH 36/38] remove get unspent notes (#4580) --- ironfish/src/wallet/wallet.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index f3e8a75afe..956e0d1c36 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -852,22 +852,6 @@ export class Wallet { return account.getBalance(assetId, confirmations) } - private async *getUnspentNotes( - account: Account, - assetId: Buffer, - options?: { - confirmations?: number - }, - ): AsyncGenerator { - const confirmations = options?.confirmations ?? this.config.get('confirmations') - - for await (const decryptedNote of account.getUnspentNotes(assetId, { - confirmations, - })) { - yield decryptedNote - } - } - async send(options: { account: Account outputs: TransactionOutput[] From aa762f8ac1183f89a5fa1812e5fe54ac9b75fd4b Mon Sep 17 00:00:00 2001 From: jowparks Date: Mon, 22 Jan 2024 16:37:40 -0800 Subject: [PATCH 37/38] Unsigned transaction napi (#4563) * unsigned transaction napi * adds unsigned transaction napi binding and test to verify ser/de works on JS side --- ironfish-rust-nodejs/index.d.ts | 5 ++++ ironfish-rust-nodejs/index.js | 3 +- .../src/structs/transaction.rs | 26 +++++++++++++++++ .../tests/unsigned.test.slow.ts | 28 +++++++++++++++++++ 4 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 ironfish-rust-nodejs/tests/unsigned.test.slow.ts diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index 4514adfd9a..23c6f0cc17 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -226,6 +226,11 @@ export class Transaction { build(proofGenerationKeyStr: string, viewKeyStr: string, outgoingViewKeyStr: string, publicAddressStr: string, intendedTransactionFee: bigint, changeGoesTo?: string | undefined | null): Buffer setExpiration(sequence: number): void } +export type NativeUnsignedTransaction = UnsignedTransaction +export class UnsignedTransaction { + constructor(jsBytes: Buffer) + serialize(): Buffer +} export class FoundBlockResult { randomness: string miningRequestId: number diff --git a/ironfish-rust-nodejs/index.js b/ironfish-rust-nodejs/index.js index df7b6811f1..8f88cd7828 100644 --- a/ironfish-rust-nodejs/index.js +++ b/ironfish-rust-nodejs/index.js @@ -252,7 +252,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { FishHashContext, roundOne, 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, LanguageCode, generateKey, spendingKeyToWords, wordsToSpendingKey, generateKeyFromPrivateKey, initializeSapling, FoundBlockResult, ThreadPoolHandler, isValidPublicAddress } = nativeBinding +const { FishHashContext, roundOne, 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 } = nativeBinding module.exports.FishHashContext = FishHashContext module.exports.roundOne = roundOne @@ -292,6 +292,7 @@ module.exports.LATEST_TRANSACTION_VERSION = LATEST_TRANSACTION_VERSION module.exports.TransactionPosted = TransactionPosted module.exports.Transaction = Transaction module.exports.verifyTransactions = verifyTransactions +module.exports.UnsignedTransaction = UnsignedTransaction module.exports.LanguageCode = LanguageCode module.exports.generateKey = generateKey module.exports.spendingKeyToWords = spendingKeyToWords diff --git a/ironfish-rust-nodejs/src/structs/transaction.rs b/ironfish-rust-nodejs/src/structs/transaction.rs index 97d181fe01..6e1d30951d 100644 --- a/ironfish-rust-nodejs/src/structs/transaction.rs +++ b/ironfish-rust-nodejs/src/structs/transaction.rs @@ -7,6 +7,7 @@ use std::cell::RefCell; use std::convert::TryInto; use ironfish::assets::asset_identifier::AssetIdentifier; +use ironfish::transaction::unsigned::UnsignedTransaction; use ironfish::transaction::{ batch_verify_transactions, TransactionVersion, TRANSACTION_EXPIRATION_SIZE, TRANSACTION_FEE_SIZE, TRANSACTION_PUBLIC_KEY_SIZE, TRANSACTION_SIGNATURE_SIZE, @@ -371,3 +372,28 @@ pub fn verify_transactions(serialized_transactions: Vec) -> Result Result { + let bytes = js_bytes.into_value()?; + + let transaction = UnsignedTransaction::read(bytes.as_ref()).map_err(to_napi_err)?; + + Ok(NativeUnsignedTransaction { transaction }) + } + + #[napi] + pub fn serialize(&self) -> Result { + let mut vec: Vec = vec![]; + self.transaction.write(&mut vec).map_err(to_napi_err)?; + + Ok(Buffer::from(vec)) + } +} diff --git a/ironfish-rust-nodejs/tests/unsigned.test.slow.ts b/ironfish-rust-nodejs/tests/unsigned.test.slow.ts new file mode 100644 index 0000000000..59f4827b20 --- /dev/null +++ b/ironfish-rust-nodejs/tests/unsigned.test.slow.ts @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { UnsignedTransaction } from ".." +import { Asset, Transaction, generateKey } from ".." + +describe('UnsignedTransaction', () => { + describe('ser/de', () => { + it('can create an unsigned tx and deserialize it', () => { + const key = generateKey() + const asset = new Asset(key.publicAddress, 'testcoin', '') + const proposedTx = new Transaction(2) + proposedTx.mint(asset, 5n) + const unsignedTxBuffer = proposedTx.build( + key.proofGenerationKey, + key.viewKey, + key.outgoingViewKey, + key.publicAddress, + 0n, + ) + + const unsignedTx = new UnsignedTransaction(unsignedTxBuffer) + expect(unsignedTx.serialize()).toEqual(unsignedTxBuffer) + + }) + }) +}) From 5222867aa3ac5fe3fa2455581472a2e3cfae343c Mon Sep 17 00:00:00 2001 From: Daniel Cogan Date: Mon, 22 Jan 2024 17:04:56 -0800 Subject: [PATCH 38/38] Version bump to CLI version 1.17.0 (#4584) --- ironfish-cli/package.json | 6 +++--- ironfish-rust-nodejs/npm/darwin-arm64/package.json | 2 +- ironfish-rust-nodejs/npm/darwin-x64/package.json | 2 +- ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json | 2 +- ironfish-rust-nodejs/npm/linux-arm64-musl/package.json | 2 +- ironfish-rust-nodejs/npm/linux-x64-gnu/package.json | 2 +- ironfish-rust-nodejs/npm/linux-x64-musl/package.json | 2 +- ironfish-rust-nodejs/npm/win32-x64-msvc/package.json | 2 +- ironfish-rust-nodejs/package.json | 2 +- ironfish/package.json | 4 ++-- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index 275b7ac178..f45ed767dd 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -1,6 +1,6 @@ { "name": "ironfish", - "version": "1.16.0", + "version": "1.17.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": "1.13.0", - "@ironfish/sdk": "1.16.0", + "@ironfish/rust-nodejs": "1.14.0", + "@ironfish/sdk": "1.17.0", "@oclif/core": "1.23.1", "@oclif/plugin-help": "5.1.12", "@oclif/plugin-not-found": "2.3.1", diff --git a/ironfish-rust-nodejs/npm/darwin-arm64/package.json b/ironfish-rust-nodejs/npm/darwin-arm64/package.json index 22a208bdd9..b9c8b7f332 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": "1.13.0", + "version": "1.14.0", "os": [ "darwin" ], diff --git a/ironfish-rust-nodejs/npm/darwin-x64/package.json b/ironfish-rust-nodejs/npm/darwin-x64/package.json index ec77d86e4d..d9e2378b70 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": "1.13.0", + "version": "1.14.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 8ccac182e3..bfc8347709 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": "1.13.0", + "version": "1.14.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 eb3ce72db9..308b1ad616 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": "1.13.0", + "version": "1.14.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 d3e89f3037..db14e50641 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": "1.13.0", + "version": "1.14.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 eb94858aa7..ecd2d235bb 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": "1.13.0", + "version": "1.14.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 6c1c11f1ac..26423ad816 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": "1.13.0", + "version": "1.14.0", "os": [ "win32" ], diff --git a/ironfish-rust-nodejs/package.json b/ironfish-rust-nodejs/package.json index 7c44901951..9d2f50320d 100644 --- a/ironfish-rust-nodejs/package.json +++ b/ironfish-rust-nodejs/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs", - "version": "1.13.0", + "version": "1.14.0", "description": "Node.js bindings for Rust code required by the Iron Fish SDK", "main": "index.js", "types": "index.d.ts", diff --git a/ironfish/package.json b/ironfish/package.json index 3fb843e770..2e679318ce 100644 --- a/ironfish/package.json +++ b/ironfish/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/sdk", - "version": "1.16.0", + "version": "1.17.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": "1.13.0", + "@ironfish/rust-nodejs": "1.14.0", "@napi-rs/blake-hash": "1.3.3", "axios": "0.21.4", "bech32": "2.0.0",