Skip to content

Commit

Permalink
fix(spend): support multiple inputs with different keys
Browse files Browse the repository at this point in the history
  • Loading branch information
maqi authored and grumbach committed Jul 29, 2024
1 parent abc3df2 commit 75afa35
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 29 deletions.
1 change: 1 addition & 0 deletions sn_client/src/audit/tests/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
40 changes: 32 additions & 8 deletions sn_node/tests/double_spend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,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.
Expand Down Expand Up @@ -121,7 +127,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
Expand Down Expand Up @@ -153,6 +160,7 @@ async fn genesis_double_spend_fail() -> Result<()> {
vec![recipient],
change_addr,
reason,
None,
)?;

// send the transfer to the network which should reject it
Expand Down Expand Up @@ -191,6 +199,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...");
Expand All @@ -215,8 +224,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)
Expand All @@ -237,8 +251,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?;
Expand Down Expand Up @@ -271,6 +290,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)
Expand Down Expand Up @@ -338,6 +358,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...");
Expand Down Expand Up @@ -367,6 +388,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,
)?;

info!("spend B to C: {:?}", transfer_to_c.all_spend_requests);
Expand Down Expand Up @@ -394,6 +416,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)
Expand Down Expand Up @@ -424,6 +447,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

info!("spend B to Y: {:?}", transfer_to_y.all_spend_requests);
Expand Down
23 changes: 15 additions & 8 deletions sn_node/tests/spend_simulation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,15 +357,19 @@ async fn inner_handle_action(
info!(
"{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();
Expand Down Expand Up @@ -411,6 +415,7 @@ async fn inner_handle_action(
vec![to],
wallet.address(),
SpendReason::default(),
None,
)?;
info!("{our_id} double spending transfer: {transfer:?}");

Expand Down Expand Up @@ -486,11 +491,12 @@ async fn handle_wallet_task_result(
// mark the input cashnotes as spent
info!("{id} marking inputs {:?} as spent", 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.
Expand Down Expand Up @@ -694,6 +700,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");
Expand Down
8 changes: 8 additions & 0 deletions sn_transfers/benches/reissue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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");

Expand Down Expand Up @@ -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");

Expand Down
4 changes: 4 additions & 0 deletions sn_transfers/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,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),
Expand Down
6 changes: 0 additions & 6 deletions sn_transfers/src/genesis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
106 changes: 101 additions & 5 deletions sn_transfers/src/transfers/offline_transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
let total_output_amount = recipients
.iter()
Expand All @@ -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<()> {
Expand Down Expand Up @@ -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<OfflineTransfer> {
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::<Vec<_>>(),
);
}

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.
Expand All @@ -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()
Expand Down
Loading

0 comments on commit 75afa35

Please sign in to comment.