|
1 | 1 | use bitcoin::{FeeRate, Psbt}; |
| 2 | +use error::IdenticalProposalError; |
2 | 3 |
|
3 | 4 | use super::error::InputContributionError; |
4 | 5 | use super::{v1, v2, Error, ImplementationError, InputPair}; |
@@ -36,6 +37,30 @@ impl UncheckedProposalBuilder { |
36 | 37 | if !params.optimistic_merge { |
37 | 38 | return Err(InternalMultipartyError::OptimisticMergeNotSupported.into()); |
38 | 39 | } |
| 40 | + |
| 41 | + if let Some(duplicate_context) = |
| 42 | + self.proposals.iter().find(|c| c.context == proposal.context) |
| 43 | + { |
| 44 | + return Err(InternalMultipartyError::IdenticalProposals( |
| 45 | + IdenticalProposalError::IdenticalContexts( |
| 46 | + Box::new(duplicate_context.id()), |
| 47 | + Box::new(proposal.id()), |
| 48 | + ), |
| 49 | + ) |
| 50 | + .into()); |
| 51 | + }; |
| 52 | + |
| 53 | + if let Some(duplicate_psbt) = |
| 54 | + self.proposals.iter().find(|psbt| psbt.v1.psbt == proposal.v1.psbt) |
| 55 | + { |
| 56 | + return Err(InternalMultipartyError::IdenticalProposals( |
| 57 | + IdenticalProposalError::IdenticalPsbts( |
| 58 | + Box::new(duplicate_psbt.v1.psbt.clone()), |
| 59 | + Box::new(proposal.v1.psbt.clone()), |
| 60 | + ), |
| 61 | + ) |
| 62 | + .into()); |
| 63 | + } |
39 | 64 | Ok(()) |
40 | 65 | } |
41 | 66 |
|
@@ -218,6 +243,29 @@ impl FinalizedProposal { |
218 | 243 | InternalMultipartyError::ProposalVersionNotSupported(proposal.v1.params.v).into() |
219 | 244 | ); |
220 | 245 | } |
| 246 | + if let Some(duplicate_context) = |
| 247 | + self.v2_proposals.iter().find(|c| c.context == proposal.context) |
| 248 | + { |
| 249 | + return Err(InternalMultipartyError::IdenticalProposals( |
| 250 | + IdenticalProposalError::IdenticalContexts( |
| 251 | + Box::new(duplicate_context.id()), |
| 252 | + Box::new(proposal.id()), |
| 253 | + ), |
| 254 | + ) |
| 255 | + .into()); |
| 256 | + }; |
| 257 | + |
| 258 | + if let Some(duplicate_psbt) = |
| 259 | + self.v2_proposals.iter().find(|psbt| psbt.v1.psbt == proposal.v1.psbt) |
| 260 | + { |
| 261 | + return Err(InternalMultipartyError::IdenticalProposals( |
| 262 | + IdenticalProposalError::IdenticalPsbts( |
| 263 | + Box::new(duplicate_psbt.v1.psbt.clone()), |
| 264 | + Box::new(proposal.v1.psbt.clone()), |
| 265 | + ), |
| 266 | + ) |
| 267 | + .into()); |
| 268 | + } |
221 | 269 | Ok(()) |
222 | 270 | } |
223 | 271 |
|
@@ -253,51 +301,120 @@ impl FinalizedProposal { |
253 | 301 | mod test { |
254 | 302 |
|
255 | 303 | use std::any::{Any, TypeId}; |
256 | | - |
257 | | - use payjoin_test_utils::{BoxError, PARSED_ORIGINAL_PSBT}; |
258 | | - |
259 | | - use super::{v1, v2, FinalizedProposal, UncheckedProposalBuilder, SUPPORTED_VERSIONS}; |
| 304 | + use std::str::FromStr; |
| 305 | + |
| 306 | + use bitcoin::Psbt; |
| 307 | + use payjoin_test_utils::{ |
| 308 | + BoxError, MULTIPARTY_ORIGINAL_PSBT_ONE, MULTIPARTY_ORIGINAL_PSBT_TWO, |
| 309 | + }; |
| 310 | + |
| 311 | + use super::error::IdenticalProposalError; |
| 312 | + use super::{ |
| 313 | + v1, v2, FinalizedProposal, InternalMultipartyError, MultipartyError, |
| 314 | + UncheckedProposalBuilder, SUPPORTED_VERSIONS, |
| 315 | + }; |
260 | 316 | use crate::receive::optional_parameters::Params; |
261 | | - use crate::receive::v2::test::SHARED_CONTEXT; |
| 317 | + use crate::receive::v2::test::{SHARED_CONTEXT, SHARED_CONTEXT_TWO}; |
262 | 318 |
|
263 | | - fn multiparty_proposal_from_test_vector() -> v1::UncheckedProposal { |
| 319 | + fn multiparty_proposals() -> Vec<v1::UncheckedProposal> { |
264 | 320 | let pairs = url::form_urlencoded::parse("v=2&optimisticmerge=true".as_bytes()); |
265 | 321 | let params = Params::from_query_pairs(pairs, SUPPORTED_VERSIONS) |
266 | 322 | .expect("Could not parse from query pairs"); |
267 | | - v1::UncheckedProposal { psbt: PARSED_ORIGINAL_PSBT.clone(), params } |
| 323 | + |
| 324 | + [MULTIPARTY_ORIGINAL_PSBT_ONE, MULTIPARTY_ORIGINAL_PSBT_TWO] |
| 325 | + .iter() |
| 326 | + .map(|psbt_str| v1::UncheckedProposal { |
| 327 | + psbt: Psbt::from_str(psbt_str).expect("known psbt should parse"), |
| 328 | + params: params.clone(), |
| 329 | + }) |
| 330 | + .collect() |
268 | 331 | } |
269 | 332 |
|
270 | 333 | #[test] |
271 | | - fn test_build_multiparty() -> Result<(), BoxError> { |
| 334 | + fn test_single_context_multiparty() -> Result<(), BoxError> { |
272 | 335 | let proposal_one = v2::UncheckedProposal { |
273 | | - v1: multiparty_proposal_from_test_vector(), |
| 336 | + v1: multiparty_proposals()[0].clone(), |
| 337 | + context: SHARED_CONTEXT.clone(), |
| 338 | + }; |
| 339 | + let mut multiparty = UncheckedProposalBuilder::new(); |
| 340 | + multiparty.add(proposal_one)?; |
| 341 | + match multiparty.build() { |
| 342 | + Ok(_) => panic!("multiparty has two identical participants and should error"), |
| 343 | + Err(e) => assert_eq!( |
| 344 | + e.to_string(), |
| 345 | + MultipartyError::from(InternalMultipartyError::NotEnoughProposals).to_string() |
| 346 | + ), |
| 347 | + } |
| 348 | + Ok(()) |
| 349 | + } |
| 350 | + |
| 351 | + #[test] |
| 352 | + fn test_duplicate_context_multiparty() -> Result<(), BoxError> { |
| 353 | + let proposal_one = v2::UncheckedProposal { |
| 354 | + v1: multiparty_proposals()[0].clone(), |
274 | 355 | context: SHARED_CONTEXT.clone(), |
275 | 356 | }; |
276 | 357 | let proposal_two = v2::UncheckedProposal { |
277 | | - v1: multiparty_proposal_from_test_vector(), |
| 358 | + v1: multiparty_proposals()[1].clone(), |
278 | 359 | context: SHARED_CONTEXT.clone(), |
279 | 360 | }; |
280 | | - let mut multiparty = UncheckedProposalBuilder::new(); |
281 | | - multiparty.add(proposal_one)?; |
282 | | - multiparty.add(proposal_two)?; |
283 | | - let unchecked_proposal = multiparty.build(); |
284 | | - assert!(unchecked_proposal?.contexts.len() == 2); |
| 361 | + let mut multiparty = UncheckedProposalBuilder::new().add(proposal_one.clone())?; |
| 362 | + match multiparty.add(proposal_two.clone()) { |
| 363 | + Ok(_) => panic!("multiparty has two identical contexts and should error"), |
| 364 | + Err(e) => assert_eq!( |
| 365 | + e.to_string(), |
| 366 | + MultipartyError::from(InternalMultipartyError::IdenticalProposals( |
| 367 | + IdenticalProposalError::IdenticalContexts( |
| 368 | + Box::new(proposal_one.id()), |
| 369 | + Box::new(proposal_two.id()) |
| 370 | + ) |
| 371 | + )) |
| 372 | + .to_string() |
| 373 | + ), |
| 374 | + } |
| 375 | + Ok(()) |
| 376 | + } |
| 377 | + |
| 378 | + #[test] |
| 379 | + fn test_duplicate_psbt_multiparty() -> Result<(), BoxError> { |
| 380 | + let proposal_one = v2::UncheckedProposal { |
| 381 | + v1: multiparty_proposals()[0].clone(), |
| 382 | + context: SHARED_CONTEXT.clone(), |
| 383 | + }; |
| 384 | + let proposal_two = v2::UncheckedProposal { |
| 385 | + v1: multiparty_proposals()[0].clone(), |
| 386 | + context: SHARED_CONTEXT_TWO.clone(), |
| 387 | + }; |
| 388 | + let mut multiparty = UncheckedProposalBuilder::new().add(proposal_one.clone())?; |
| 389 | + match multiparty.add(proposal_two.clone()) { |
| 390 | + Ok(_) => panic!("multiparty has two identical psbts and should error"), |
| 391 | + Err(e) => assert_eq!( |
| 392 | + e.to_string(), |
| 393 | + MultipartyError::from(InternalMultipartyError::IdenticalProposals( |
| 394 | + IdenticalProposalError::IdenticalPsbts( |
| 395 | + Box::new(proposal_one.v1.psbt), |
| 396 | + Box::new(proposal_two.v1.psbt) |
| 397 | + ) |
| 398 | + )) |
| 399 | + .to_string() |
| 400 | + ), |
| 401 | + } |
285 | 402 | Ok(()) |
286 | 403 | } |
287 | 404 |
|
288 | 405 | #[test] |
289 | 406 | fn finalize_multiparty() -> Result<(), BoxError> { |
290 | 407 | use crate::psbt::PsbtExt; |
291 | 408 | let proposal_one = v2::UncheckedProposal { |
292 | | - v1: multiparty_proposal_from_test_vector(), |
| 409 | + v1: multiparty_proposals()[0].clone(), |
293 | 410 | context: SHARED_CONTEXT.clone(), |
294 | 411 | }; |
295 | 412 | let proposal_two = v2::UncheckedProposal { |
296 | | - v1: multiparty_proposal_from_test_vector(), |
297 | | - context: SHARED_CONTEXT.clone(), |
| 413 | + v1: multiparty_proposals()[1].clone(), |
| 414 | + context: SHARED_CONTEXT_TWO.clone(), |
298 | 415 | }; |
299 | 416 | let mut finalized_multiparty = FinalizedProposal::new(); |
300 | | - finalized_multiparty.add(proposal_one.clone())?; |
| 417 | + finalized_multiparty.add(proposal_one)?; |
301 | 418 | assert_eq!(finalized_multiparty.v2()[0].type_id(), TypeId::of::<v2::UncheckedProposal>()); |
302 | 419 |
|
303 | 420 | finalized_multiparty.add(proposal_two)?; |
|
0 commit comments