diff --git a/crates/rbitcoin-rpc/src/methods/mempool.rs b/crates/rbitcoin-rpc/src/methods/mempool.rs index 7314bbb93..b11d87f17 100644 --- a/crates/rbitcoin-rpc/src/methods/mempool.rs +++ b/crates/rbitcoin-rpc/src/methods/mempool.rs @@ -608,7 +608,31 @@ pub(crate) fn testmempoolaccept(ctx: &RpcContext, params: &RpcParams) -> Result< } let mut out = Vec::new(); for tx in decoded { - let txid = hash_hex_display(&tx.compute_txid().to_byte_array()); + let raw_txid = tx.compute_txid(); + let txid_bytes = raw_txid.to_byte_array(); + let txid = hash_hex_display(&txid_bytes); + let wtxid_str = hash_hex_display(&tx.compute_wtxid().to_byte_array()); + + // #628: active-chain confirmed -> txn-already-known (reorg-safe) + let confirmed_active = match ctx.query.tx_fk_by_txid_tip(&txid_bytes) { + Ok(Some(fk)) => ctx + .query + .store() + .is_confirmed_strong(fk) + .map_err(|e| rpc_error(ERR_MISC, format!("query failed: {e}")))?, + Ok(None) => false, + Err(e) => return Err(rpc_error(ERR_MISC, format!("query failed: {e}"))), + }; + if confirmed_active { + out.push(serde_json::json!({ + "txid": txid, + "wtxid": wtxid_str, + "allowed": false, + "reject-reason": "txn-already-known", + })); + continue; + } + match mp.test_accept(&tx) { Ok(r) => { let wtxid = hash_hex_display(&tx.compute_wtxid().to_byte_array()); diff --git a/crates/rbitcoin-rpc/src/methods_tests.rs b/crates/rbitcoin-rpc/src/methods_tests.rs index 6f508fe5c..4790323b4 100644 --- a/crates/rbitcoin-rpc/src/methods_tests.rs +++ b/crates/rbitcoin-rpc/src/methods_tests.rs @@ -4670,3 +4670,67 @@ fn dispatch_wrong_json_types_are_param_errors() { } let _ = std::fs::remove_dir_all(&dir); } + +// #628: active-chain vs mempool distinction - shipped JSON asserts +#[test] +fn testmempoolaccept_628_confirmed_is_already_known() { + let (ctx, _dir, hub) = ctx_regtest_hub(); + let miner = TestMiner(hub); + for _ in 0..101 { + miner + .generate_to_script(1, ScriptBuf::from_bytes(vec![0x51]), vec![]) + .unwrap(); + } + let (hex, tx) = mature_coinbase_spend_hex(&ctx, generated_coinbase_value(&ctx, 100) - 1000); + miner + .generate_to_script(1, ScriptBuf::from_bytes(vec![0x51]), vec![tx]) + .unwrap(); + let res = dispatch(&ctx, "testmempoolaccept", vec![json!([hex]), json!(0)]).unwrap(); + assert_eq!(res[0]["allowed"], json!(false)); + assert_eq!(res[0]["reject-reason"], json!("txn-already-known")); +} +#[test] +fn testmempoolaccept_628_mempool_duplicate_is_already_in_mempool() { + let (ctx2, _dir2, hub) = ctx_regtest_hub(); + let miner = TestMiner(hub); + for _ in 0..101 { + miner + .generate_to_script(1, ScriptBuf::from_bytes(vec![0x51]), vec![]) + .unwrap(); + } + let (hex, tx) = mature_coinbase_spend_hex(&ctx2, generated_coinbase_value(&ctx2, 100) - 1000); + let mp = ctx2.mempool.as_ref().unwrap(); + let _ = mp.accept_tx_from(&tx, None).unwrap(); + let res = dispatch(&ctx2, "testmempoolaccept", vec![json!([hex]), json!(0)]).unwrap(); + assert_eq!(res[0]["allowed"], json!(false)); + assert_eq!(res[0]["reject-reason"], json!("txn-already-in-mempool")); +} +#[test] +fn testmempoolaccept_628_reorged_archive_not_already_known() { + let (ctx, _dir, hub) = ctx_regtest_hub(); + let miner = TestMiner(hub); + for _ in 0..101 { + miner + .generate_to_script(1, ScriptBuf::from_bytes(vec![0x51]), vec![]) + .unwrap(); + } + let (hex, tx) = mature_coinbase_spend_hex(&ctx, generated_coinbase_value(&ctx, 100) - 1000); + miner + .generate_to_script(1, ScriptBuf::from_bytes(vec![0x51]), vec![tx]) + .unwrap(); + let tip_hash = dispatch(&ctx, "getbestblockhash", vec![]).unwrap(); + let _ = dispatch(&ctx, "invalidateblock", vec![tip_hash.clone()]).unwrap(); + let res = dispatch( + &ctx, + "testmempoolaccept", + vec![json!([hex.clone()]), json!(0)], + ) + .unwrap(); + assert_eq!(res[0]["allowed"], json!(true), "{res}"); + assert_ne!( + res[0]["reject-reason"], + json!("txn-already-known"), + "archive-only must not be known" + ); + let _ = dispatch(&ctx, "reconsiderblock", vec![tip_hash]).unwrap(); +}