From 7167cf65971f6ded3b2e6349e31fb6c5a4e6d296 Mon Sep 17 00:00:00 2001 From: qima Date: Tue, 9 Jul 2024 20:43:45 +0800 Subject: [PATCH] fix(spend): support multiple inputs with different keys --- sn_client/src/audit/tests/setup.rs | 1 + sn_node/tests/double_spend.rs | 40 +++++-- sn_node/tests/spend_simulation.rs | 36 +++--- sn_transfers/benches/reissue.rs | 8 ++ sn_transfers/src/error.rs | 4 + sn_transfers/src/genesis.rs | 6 - .../src/transfers/offline_transfer.rs | 106 +++++++++++++++++- sn_transfers/src/wallet/hot_wallet.rs | 26 ++++- 8 files changed, 192 insertions(+), 35 deletions(-) diff --git a/sn_client/src/audit/tests/setup.rs b/sn_client/src/audit/tests/setup.rs index 2b17184c6a..dca1400a84 100644 --- a/sn_client/src/audit/tests/setup.rs +++ b/sn_client/src/audit/tests/setup.rs @@ -114,6 +114,7 @@ impl MockNetwork { recipient, from_wallet.sk.main_pubkey(), SpendReason::default(), + None, ) .map_err(|e| eyre!("failed to create transfer: {}", e))?; let spends = transfer.all_spend_requests; diff --git a/sn_node/tests/double_spend.rs b/sn_node/tests/double_spend.rs index 4e404bfbf7..4d26786a53 100644 --- a/sn_node/tests/double_spend.rs +++ b/sn_node/tests/double_spend.rs @@ -52,9 +52,15 @@ async fn cash_note_transfer_double_spend_fail() -> Result<()> { let to2_unique_key = (amount, to2, DerivationIndex::random(&mut rng)); let to3_unique_key = (amount, to3, DerivationIndex::random(&mut rng)); - let transfer_to_2 = - OfflineTransfer::new(some_cash_notes, vec![to2_unique_key], to1, reason.clone())?; - let transfer_to_3 = OfflineTransfer::new(same_cash_notes, vec![to3_unique_key], to1, reason)?; + let transfer_to_2 = OfflineTransfer::new( + some_cash_notes, + vec![to2_unique_key], + to1, + reason.clone(), + None, + )?; + let transfer_to_3 = + OfflineTransfer::new(same_cash_notes, vec![to3_unique_key], to1, reason, None)?; // send both transfers to the network // upload won't error out, only error out during verification. @@ -116,7 +122,8 @@ async fn genesis_double_spend_fail() -> Result<()> { ); let change_addr = second_wallet_addr; let reason = SpendReason::default(); - let transfer = OfflineTransfer::new(genesis_cashnote, vec![recipient], change_addr, reason)?; + let transfer = + OfflineTransfer::new(genesis_cashnote, vec![recipient], change_addr, reason, None)?; // send the transfer to the network which will mark genesis as a double spent // making its direct descendants unspendable @@ -148,6 +155,7 @@ async fn genesis_double_spend_fail() -> Result<()> { vec![recipient], change_addr, reason, + None, )?; // send the transfer to the network which should reject it @@ -186,6 +194,7 @@ async fn poisoning_old_spend_should_not_affect_descendant() -> Result<()> { vec![to_2_unique_key], to1, reason.clone(), + None, )?; info!("Sending 1->2 to the network..."); @@ -210,8 +219,13 @@ async fn poisoning_old_spend_should_not_affect_descendant() -> Result<()> { wallet_22.address(), DerivationIndex::random(&mut rng), ); - let transfer_to_22 = - OfflineTransfer::new(cash_notes_2, vec![to_22_unique_key], to2, reason.clone())?; + let transfer_to_22 = OfflineTransfer::new( + cash_notes_2, + vec![to_22_unique_key], + to2, + reason.clone(), + None, + )?; client .send_spends(transfer_to_22.all_spend_requests.iter(), false) @@ -232,8 +246,13 @@ async fn poisoning_old_spend_should_not_affect_descendant() -> Result<()> { wallet_3.address(), DerivationIndex::random(&mut rng), ); - let transfer_to_3 = - OfflineTransfer::new(cash_notes_1, vec![to_3_unique_key], to1, reason.clone())?; // reuse the old cash notes + let transfer_to_3 = OfflineTransfer::new( + cash_notes_1, + vec![to_3_unique_key], + to1, + reason.clone(), + None, + )?; // reuse the old cash notes client .send_spends(transfer_to_3.all_spend_requests.iter(), false) .await?; @@ -260,6 +279,7 @@ async fn poisoning_old_spend_should_not_affect_descendant() -> Result<()> { vec![to_222_unique_key], wallet_22.address(), reason, + None, )?; client .send_spends(transfer_to_222.all_spend_requests.iter(), false) @@ -301,6 +321,7 @@ async fn parent_and_child_double_spends_should_lead_to_cashnote_being_invalid() vec![to_b_unique_key], wallet_a.address(), reason.clone(), + None, )?; info!("Sending A->B to the network..."); @@ -330,6 +351,7 @@ async fn parent_and_child_double_spends_should_lead_to_cashnote_being_invalid() vec![to_c_unique_key], wallet_b.address(), reason.clone(), + None, )?; client @@ -356,6 +378,7 @@ async fn parent_and_child_double_spends_should_lead_to_cashnote_being_invalid() vec![to_x_unique_key], wallet_a.address(), reason.clone(), + None, )?; // reuse the old cash notes client .send_spends(transfer_to_x.all_spend_requests.iter(), false) @@ -383,6 +406,7 @@ async fn parent_and_child_double_spends_should_lead_to_cashnote_being_invalid() vec![to_y_unique_key], wallet_b.address(), reason.clone(), + None, )?; // reuse the old cash notes client .send_spends(transfer_to_y.all_spend_requests.iter(), false) diff --git a/sn_node/tests/spend_simulation.rs b/sn_node/tests/spend_simulation.rs index ba46f0b4b2..d695d69bf9 100644 --- a/sn_node/tests/spend_simulation.rs +++ b/sn_node/tests/spend_simulation.rs @@ -165,12 +165,13 @@ async fn spend_simulation() -> Result<()> { if let Some(tx) = tx { let mut input_cash_notes = Vec::new(); for input in &tx.inputs { - let (status, cashnote) = state - .cashnote_tracker - .get_mut(&input.unique_pubkey) - .ok_or_eyre("Input spend not tracked")?; - *status = SpendStatus::Poisoned; - input_cash_notes.push(cashnote.clone()); + // Transaction may contains the `middle payment` + if let Some((status, cashnote)) = + state.cashnote_tracker.get_mut(&input.unique_pubkey) + { + *status = SpendStatus::Poisoned; + input_cash_notes.push(cashnote.clone()); + } } info!( "Wallet {id} is attempting to poison a old spend. Marking inputs {:?} as Poisoned", @@ -289,15 +290,19 @@ async fn inner_handle_action( info!( "TestWallet {our_id} Available CashNotes for local send: {:?}", available_cash_notes - .iter() - .map(|(c, _)| c.unique_pubkey()) - .collect_vec() ); + let mut rng = &mut rand::rngs::OsRng; + let derivation_index = DerivationIndex::random(&mut rng); let transfer = OfflineTransfer::new( available_cash_notes, recipients, wallet.address(), SpendReason::default(), + Some(( + wallet.key().main_pubkey(), + derivation_index, + wallet.key().derive_key(&derivation_index), + )), )?; let recipient_cash_notes = transfer.cash_notes_for_recipient.clone(); let change = transfer.change_cash_note.clone(); @@ -335,6 +340,7 @@ async fn inner_handle_action( vec![to], wallet.address(), SpendReason::default(), + None, )?; info!("TestWallet {our_id} double spending transfer: {transfer:?}"); @@ -399,11 +405,12 @@ async fn handle_wallet_task_result( transaction.inputs ); for input in &transaction.inputs { - let (status, _cashnote) = state - .cashnote_tracker - .get_mut(&input.unique_pubkey) - .ok_or_eyre("Input spend not tracked")?; - *status = SpendStatus::Spent; + // Transaction may contains the `middle payment` + if let Some((status, _cashnote)) = + state.cashnote_tracker.get_mut(&input.unique_pubkey) + { + *status = SpendStatus::Spent; + } } // track the change cashnote that is stored by our wallet. @@ -579,6 +586,7 @@ async fn init_state(count: usize) -> Result<(Client, State)> { recipients, first_wallet.address(), reason.clone(), + None, )?; info!("Sending transfer for all wallets and verifying them"); diff --git a/sn_transfers/benches/reissue.rs b/sn_transfers/benches/reissue.rs index 8788f3c207..179c81d421 100644 --- a/sn_transfers/benches/reissue.rs +++ b/sn_transfers/benches/reissue.rs @@ -41,6 +41,7 @@ fn bench_reissue_1_to_100(c: &mut Criterion) { recipients, starting_main_key.main_pubkey(), SpendReason::default(), + None, ) .expect("transfer to succeed"); @@ -97,6 +98,7 @@ fn bench_reissue_100_to_1(c: &mut Criterion) { recipients, starting_main_key.main_pubkey(), SpendReason::default(), + None, ) .expect("transfer to succeed"); @@ -134,12 +136,18 @@ fn bench_reissue_100_to_1(c: &mut Criterion) { DerivationIndex::random(&mut rng), )]; + let derivation_index = DerivationIndex::random(&mut rng); // create transfer to merge all of the cashnotes into one let many_to_one_transfer = OfflineTransfer::new( many_cashnotes, one_single_recipient, starting_main_key.main_pubkey(), SpendReason::default(), + Some(( + starting_main_key.main_pubkey(), + derivation_index, + starting_main_key.derive_key(&derivation_index), + )), ) .expect("transfer to succeed"); diff --git a/sn_transfers/src/error.rs b/sn_transfers/src/error.rs index b901bcd4a3..6790822ef9 100644 --- a/sn_transfers/src/error.rs +++ b/sn_transfers/src/error.rs @@ -85,6 +85,10 @@ pub enum TransferError { TransferDeserializationFailed, #[error("The OutputPurpose bearing an invlalid length")] OutputPurposeTooShort, + #[error("Multiple inputs from different keys without a middle-addr")] + MultipleInputsWithoutMiddleAddr, + #[error("Multiple inputs from different keys without a middle payment")] + MultipleInputsWithoutMiddlePayment, #[error("Bls error: {0}")] Blsttc(#[from] bls::error::Error), diff --git a/sn_transfers/src/genesis.rs b/sn_transfers/src/genesis.rs index ce95820fb4..7a238105c1 100644 --- a/sn_transfers/src/genesis.rs +++ b/sn_transfers/src/genesis.rs @@ -134,12 +134,6 @@ pub fn get_genesis_sk() -> MainSecretKey { /// Return if provided Spend is genesis spend. pub fn is_genesis_spend(spend: &SignedSpend) -> bool { - info!( - "Testing genesis against genesis_input {:?} genesis_output {:?} {GENESIS_CASHNOTE_AMOUNT:?}, {:?}", - GENESIS_PK.new_unique_pubkey(&GENESIS_INPUT_DERIVATION_INDEX), - GENESIS_PK.new_unique_pubkey(&GENESIS_OUTPUT_DERIVATION_INDEX), - spend.spend - ); let bytes = spend.spend.to_bytes_for_signing(); spend.spend.unique_pubkey == *GENESIS_SPEND_UNIQUE_KEY && GENESIS_SPEND_UNIQUE_KEY.verify(&spend.derived_key_sig, bytes) diff --git a/sn_transfers/src/transfers/offline_transfer.rs b/sn_transfers/src/transfers/offline_transfer.rs index 7ef8af862e..be8c93319c 100644 --- a/sn_transfers/src/transfers/offline_transfer.rs +++ b/sn_transfers/src/transfers/offline_transfer.rs @@ -108,11 +108,16 @@ impl OfflineTransfer { /// The peers will validate each signed spend they receive, before accepting it. /// Once enough peers have accepted all the spends of the transaction, and serve /// them upon request, the transaction will be completed. + /// + /// When there are multiple inputs from different unique_pubkeys, + /// they shall all be paid into a `middle_addr` first, then from that `middle_addr` pay out + /// to recipients as normal. pub fn new( available_cash_notes: CashNotesAndSecretKey, recipients: Vec<(NanoTokens, MainPubkey, DerivationIndex)>, change_to: MainPubkey, input_reason_hash: SpendReason, + middle_addr: Option<(MainPubkey, DerivationIndex, DerivedSecretKey)>, ) -> Result { let total_output_amount = recipients .iter() @@ -135,7 +140,7 @@ impl OfflineTransfer { change: (change_amount, change_to), }; - create_offline_transfer_with(selected_inputs, input_reason_hash) + create_offline_transfer_with(selected_inputs, input_reason_hash, middle_addr) } pub fn verify(&self, main_key: &MainSecretKey) -> Result<()> { @@ -310,9 +315,103 @@ fn create_transaction_builder_with( /// to the network. When those same signed spends can be retrieved from /// enough peers in the network, the transaction will be completed. fn create_offline_transfer_with( - selected_inputs: TransferInputs, + mut selected_inputs: TransferInputs, input_reason: SpendReason, + middle_addr: Option<(MainPubkey, DerivationIndex, DerivedSecretKey)>, ) -> Result { + let mut all_spend_requests = vec![]; + + let input_keys: BTreeSet<_> = selected_inputs + .cash_notes_to_spend + .iter() + .map(|(cn, _)| cn.unique_pubkey) + .collect(); + if input_keys.len() > 1 { + info!( + "Multiple input_keys vs multiple outputs detected {:?}", + input_keys + ); + if let Some((main_pubkey, derivation_index, middle_derived_sk)) = middle_addr { + let mut middle_cash_notes = vec![]; + for input_key in input_keys { + let mut amount: u64 = 0; + let cash_notes_to_spend = selected_inputs + .cash_notes_to_spend + .iter() + .filter_map(|(cn, derived_sk)| { + if cn.unique_pubkey == input_key { + if let Ok(value) = cn.value() { + amount += value.as_nano(); + Some((cn.clone(), derived_sk.clone())) + } else { + None + } + } else { + None + } + }) + .collect(); + + let recipients = vec![(NanoTokens::from(amount), main_pubkey, derivation_index)]; + + let middle_inputs = TransferInputs { + cash_notes_to_spend, + recipients, + change: (NanoTokens::zero(), selected_inputs.change.1), + }; + + let (tx_builder, _change_id) = create_transaction_builder_with(middle_inputs)?; + let cash_note_builder = tx_builder.build(input_reason.clone()); + let signed_spends: BTreeMap<_, _> = cash_note_builder + .signed_spends() + .into_iter() + .map(|spend| (spend.unique_pubkey(), spend)) + .collect(); + + for (_, signed_spend) in signed_spends.into_iter() { + all_spend_requests.push(signed_spend.to_owned()); + } + middle_cash_notes.extend( + cash_note_builder + .build()? + .into_iter() + .map(|(cash_note, _)| cash_note) + .collect::>(), + ); + } + + info!("We now have middle cash notes: {middle_cash_notes:?}"); + + let mut parent_spends = BTreeSet::new(); + for cn in middle_cash_notes.iter() { + for parent_spend in cn.parent_spends.iter() { + let _ = parent_spends.insert(parent_spend.clone()); + } + } + + let cash_notes_to_spend = if let Some(cn) = middle_cash_notes.first() { + let merged_cn = CashNote { + unique_pubkey: cn.unique_pubkey, + parent_spends, + main_pubkey: cn.main_pubkey, + derivation_index: cn.derivation_index, + }; + info!("We now have a merged cash note: {merged_cn:?}"); + vec![(merged_cn, Some(middle_derived_sk.clone()))] + } else { + return Err(TransferError::MultipleInputsWithoutMiddlePayment); + }; + + selected_inputs = TransferInputs { + cash_notes_to_spend, + recipients: selected_inputs.recipients, + change: selected_inputs.change, + }; + } else { + return Err(TransferError::MultipleInputsWithoutMiddleAddr); + } + } + let (tx_builder, change_id) = create_transaction_builder_with(selected_inputs)?; // Finalize the tx builder to get the cash_note builder. @@ -324,13 +423,10 @@ fn create_offline_transfer_with( .map(|spend| (spend.unique_pubkey(), spend)) .collect(); - let mut all_spend_requests = vec![]; for (_, signed_spend) in signed_spends.into_iter() { all_spend_requests.push(signed_spend.to_owned()); } - // Perform validations of input tx and signed spends, - // as well as building the output CashNotes. let mut created_cash_notes: Vec<_> = cash_note_builder .build()? .into_iter() diff --git a/sn_transfers/src/wallet/hot_wallet.rs b/sn_transfers/src/wallet/hot_wallet.rs index e6b19e0757..fe4e7078ef 100644 --- a/sn_transfers/src/wallet/hot_wallet.rs +++ b/sn_transfers/src/wallet/hot_wallet.rs @@ -339,8 +339,18 @@ impl HotWallet { let reason = reason.unwrap_or_default(); - let transfer = - OfflineTransfer::new(available_cash_notes, to_unique_keys, self.address(), reason)?; + let derivation_index = DerivationIndex::random(&mut rng); + let transfer = OfflineTransfer::new( + available_cash_notes, + to_unique_keys, + self.address(), + reason, + Some(( + self.key.main_pubkey(), + derivation_index, + self.key.derive_key(&derivation_index), + )), + )?; let created_cash_notes = transfer.cash_notes_for_recipient.clone(); @@ -403,12 +413,18 @@ impl HotWallet { .into_iter() .map(|(amount, address)| (amount, address, DerivationIndex::random(&mut rng))) .collect(); + let derivation_index = DerivationIndex::random(&mut rng); let transfer = OfflineTransfer::new( available_cash_notes, to_unique_keys, self.address(), spend_reason, + Some(( + self.key.main_pubkey(), + derivation_index, + self.key.derive_key(&derivation_index), + )), )?; let signed_spends = transfer.all_spend_requests.clone(); @@ -479,11 +495,17 @@ impl HotWallet { let spend_reason = Default::default(); let start = Instant::now(); + let derivation_index = DerivationIndex::random(&mut rng); let offline_transfer = OfflineTransfer::new( available_cash_notes, recipients, self.address(), spend_reason, + Some(( + self.key.main_pubkey(), + derivation_index, + self.key.derive_key(&derivation_index), + )), )?; trace!( "local_send_storage_payment created offline_transfer with {} cashnotes in {:?}",