diff --git a/crates/sp-domains-fraud-proof/src/verification.rs b/crates/sp-domains-fraud-proof/src/verification.rs index ff88e7736c..483837cd83 100644 --- a/crates/sp-domains-fraud-proof/src/verification.rs +++ b/crates/sp-domains-fraud-proof/src/verification.rs @@ -667,23 +667,35 @@ where invalid_bundles_fraud_proof.proof_data.clone(), )?; - let is_valid_xdm = get_fraud_proof_verification_info( + let maybe_is_valid_xdm = get_fraud_proof_verification_info( H256::from_slice(bad_receipt.consensus_block_hash.as_ref()), FraudProofVerificationInfoRequest::XDMValidationCheck { domain_id: invalid_bundles_fraud_proof.domain_id, opaque_extrinsic: extrinsic, }, ) - .and_then(FraudProofVerificationInfoResponse::into_xdm_validation_check) - .ok_or(VerificationError::FailedToValidateXDM)?; - - // Proof to be considered valid only, - // If it is true invalid fraud proof then extrinsic must be an invalid xdm and - // If it is false invalid fraud proof then extrinsic must be a valid xdm - if is_valid_xdm != invalid_bundles_fraud_proof.is_true_invalid_fraud_proof { - Ok(()) + .and_then(FraudProofVerificationInfoResponse::into_xdm_validation_check); + + if let Some(is_valid_xdm) = maybe_is_valid_xdm { + // Proof to be considered valid only, + // If it is true invalid fraud proof then extrinsic must be an invalid xdm and + // If it is false invalid fraud proof then extrinsic must be a valid xdm + if is_valid_xdm != invalid_bundles_fraud_proof.is_true_invalid_fraud_proof { + Ok(()) + } else { + Err(VerificationError::InvalidProof) + } } else { - Err(VerificationError::InvalidProof) + // If this extrinsic is not an XDM, + // If it is false invalid, then bad receipt marked this extrinsic as InvalidXDM + // even though it is not an XDM, if so accept the fraud proof + if !invalid_bundles_fraud_proof.is_true_invalid_fraud_proof { + Ok(()) + } else { + // If this is a true invalid but the extrinsic is not an XDM, then reject fraud proof. + // this can happen if there is a bug in the challenger node implementation. + Err(VerificationError::InvalidProof) + } } } } diff --git a/domains/client/domain-operator/src/tests.rs b/domains/client/domain-operator/src/tests.rs index 8a6c79a85f..b9a536d9b7 100644 --- a/domains/client/domain-operator/src/tests.rs +++ b/domains/client/domain-operator/src/tests.rs @@ -1953,6 +1953,122 @@ async fn test_false_invalid_bundles_illegal_extrinsic_proof_creation_and_verific assert!(!ferdie.does_receipt_exist(bad_receipt_hash).unwrap()); } +#[tokio::test(flavor = "multi_thread")] +async fn test_false_invalid_xdm_extrinsic_proof_creation_and_verification() { + let directory = TempDir::new().expect("Must be able to create temporary directory"); + + let mut builder = sc_cli::LoggerBuilder::new(""); + builder.with_colors(false); + let _ = builder.init(); + + let tokio_handle = tokio::runtime::Handle::current(); + + // Start Ferdie + let mut ferdie = MockConsensusNode::run( + tokio_handle.clone(), + Ferdie, + BasePath::new(directory.path().join("ferdie")), + ); + + // Run Alice (a evm domain authority node) + let mut alice = domain_test_service::DomainNodeBuilder::new( + tokio_handle.clone(), + Alice, + BasePath::new(directory.path().join("alice")), + ) + .build_evm_node(Role::Authority, GENESIS_DOMAIN_ID, &mut ferdie) + .await; + + let bundle_to_tx = |opaque_bundle| { + subspace_test_runtime::UncheckedExtrinsic::new_unsigned( + pallet_domains::Call::submit_bundle { opaque_bundle }.into(), + ) + .into() + }; + + produce_blocks!(ferdie, alice, 5).await.unwrap(); + + // transfer some balance from alice + let alice_balance = alice.free_balance(Alice.to_account_id()); + let alice_nonce = alice.account_nonce(); + + let transfer_to_charlie_with_tip = alice.construct_extrinsic_with_tip( + alice_nonce, + alice_balance / 3, + pallet_balances::Call::transfer_allow_death { + dest: Charlie.to_account_id(), + value: 1, + }, + ); + + alice + .send_extrinsic(transfer_to_charlie_with_tip) + .await + .expect("Failed to send extrinsic"); + + // Produce a bundle that contains the previously sent extrinsic and record that bundle for later use + let (slot, target_bundle) = ferdie.produce_slot_and_wait_for_bundle_submission().await; + assert_eq!(target_bundle.extrinsics.len(), 1); + let bundle_extrinsic_root = target_bundle.extrinsics_root(); + produce_block_with!(ferdie.produce_block_with_slot(slot), alice) + .await + .unwrap(); + + // produce another bundle that marks the previous valid extrinsic as invalid. + let (slot, mut opaque_bundle) = ferdie.produce_slot_and_wait_for_bundle_submission().await; + + let (bad_receipt_hash, bad_submit_bundle_tx) = { + let bad_receipt = &mut opaque_bundle.sealed_header.header.receipt; + // bad receipt marks this particular bundle as invalid even though the call is not XDM + bad_receipt.inboxed_bundles = vec![InboxedBundle::invalid( + InvalidBundleType::InvalidXDM(0), + bundle_extrinsic_root, + )]; + + opaque_bundle.sealed_header.signature = Sr25519Keyring::Alice + .pair() + .sign(opaque_bundle.sealed_header.pre_hash().as_ref()) + .into(); + ( + opaque_bundle.receipt().hash::(), + bundle_to_tx(opaque_bundle), + ) + }; + + // Wait for the fraud proof that target the bad ER + let wait_for_fraud_proof_fut = ferdie.wait_for_fraud_proof(move |fp| { + if let FraudProof::InvalidBundles(proof) = fp { + if let InvalidBundleType::InvalidXDM(extrinsic_index) = proof.invalid_bundle_type { + assert!(!proof.is_true_invalid_fraud_proof); + assert_eq!(extrinsic_index, 0); + return true; + } + } + false + }); + + // Produce a consensus block that contains the `bad_submit_bundle_tx` and the bad receipt should + // be added to the consensus chain block tree + produce_block_with!( + ferdie.produce_block_with_slot_at( + slot, + ferdie.client.info().best_hash, + Some(vec![bad_submit_bundle_tx]) + ), + alice + ) + .await + .unwrap(); + assert!(ferdie.does_receipt_exist(bad_receipt_hash).unwrap()); + + let _ = wait_for_fraud_proof_fut.await; + + // Produce a consensus block that contains the fraud proof, the fraud proof wil be verified + // and executed, thus pruned the bad receipt from the block tree + ferdie.produce_blocks(1).await.unwrap(); + assert!(!ferdie.does_receipt_exist(bad_receipt_hash).unwrap()); +} + #[tokio::test(flavor = "multi_thread")] async fn test_invalid_block_fees_proof_creation() { let directory = TempDir::new().expect("Must be able to create temporary directory");