Repository: bitcoin/bitcoin (language: cpp)

Issue:
bitcoin-tx replaceable value should be optional, but isn't
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current behaviour

The help text says:

```
  replaceable(=N)
       Set RBF opt-in sequence number for input N (if not provided, opt-in all
       available inputs)
```

but when I don't provide N, I get an error:

```
$ bitcoin-tx -create in=0000000000000000000000000000000000000000000000000000000000000000:0 replaceable
error: Invalid TX input index ''
```

### Expected behaviour

I expect bitcoin-tx to act like its help string describes:

```
$ bitcoin-tx -create in=0000000000000000000000000000000000000000000000000000000000000000:0 replaceable
020000000100000000000000000000000000000000000000000000000000000000000000000000000000fdffffff0000000000
```

The fix is simple:

```
diff --git a/src/bitcoin-tx.cpp b/src/bitcoin-tx.cpp
index a05443a2e5..b9348fcad0 100644
--- a/src/bitcoin-tx.cpp
+++ b/src/bitcoin-tx.cpp
@@ -222,7 +222,7 @@ static void MutateTxRBFOptIn(CMutableTransaction& tx, const std::string& strInId
 {
     // parse requested index
     int64_t inIdx;
-    if (!ParseInt64(strInIdx, &inIdx) || inIdx < 0 || inIdx >= static_cast<int64_t>(tx.vin.size())) {
+    if (strInIdx != "" && (!ParseInt64(strInIdx, &inIdx) || inIdx < 0 || inIdx >= static_cast<int64_t>(tx.vin.size()))) {
         throw std::runtime_error("Invalid TX input index '" + strInIdx + "'");
     }
 
```

### Steps to reproduce

Run the command shown above.

### Relevant log output

_No response_

### How did you obtain Bitcoin Core

Compiled from source

### What version of Bitcoin Core are you using?

master@e3eb3aae3

### Operating system and version

Debian 12

### Machine specifications

_No response_


Discussion / hints:
Aside: shouldn't bitcoin-tx default to making replaceable transactions, to match what bitcoind and bitcoin-qt do?

Fix patch:
diff --git a/Makefile.am b/Makefile.am
index c50421dfc339e..eec498dc0e937 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -310,6 +310,10 @@ EXTRA_DIST += \
     test/util/data/txcreatesignv1.hex \
     test/util/data/txcreatesignv1.json \
     test/util/data/txcreatesignv2.hex \
+    test/util/data/txreplace1.hex \
+    test/util/data/txreplacenoinputs.hex \
+    test/util/data/txreplaceomittedn.hex \
+    test/util/data/txreplacesingleinput.hex \
     test/util/rpcauth-test.py
 
 CLEANFILES = $(OSX_ZIP) $(BITCOIN_WIN_INSTALLER)
diff --git a/src/bitcoin-tx.cpp b/src/bitcoin-tx.cpp
index 8fe2881f6f2f6..320624c419b80 100644
--- a/src/bitcoin-tx.cpp
+++ b/src/bitcoin-tx.cpp
@@ -66,7 +66,9 @@ static void SetupBitcoinTxArgs(ArgsManager &argsman)
     argsman.AddArg("outscript=VALUE:SCRIPT[:FLAGS]", "Add raw script output to TX. "
         "Optionally add the \"W\" flag to produce a pay-to-witness-script-hash output. "
         "Optionally add the \"S\" flag to wrap the output in a pay-to-script-hash.", ArgsManager::ALLOW_ANY, OptionsCategory::COMMANDS);
-    argsman.AddArg("replaceable(=N)", "Set RBF opt-in sequence number for input N (if not provided, opt-in all available inputs)", ArgsManager::ALLOW_ANY, OptionsCategory::COMMANDS);
+    argsman.AddArg("replaceable(=N)", "Sets Replace-By-Fee (RBF) opt-in sequence number for input N. "
+        "If N is not provided, the command attempts to opt-in all available inputs for RBF. "
+        "If the transaction has no inputs, this option is ignored.", ArgsManager::ALLOW_ANY, OptionsCategory::COMMANDS);
     argsman.AddArg("sign=SIGHASH-FLAGS", "Add zero or more signatures to transaction. "
         "This command requires JSON registers:"
         "prevtxs=JSON object, "
@@ -223,8 +225,8 @@ static void MutateTxLocktime(CMutableTransaction& tx, const std::string& cmdVal)
 static void MutateTxRBFOptIn(CMutableTransaction& tx, const std::string& strInIdx)
 {
     // parse requested index
-    int64_t inIdx;
-    if (!ParseInt64(strInIdx, &inIdx) || inIdx < 0 || inIdx >= static_cast<int64_t>(tx.vin.size())) {
+    int64_t inIdx = -1;
+    if (strInIdx != "" && (!ParseInt64(strInIdx, &inIdx) || inIdx < 0 || inIdx >= static_cast<int64_t>(tx.vin.size()))) {
         throw std::runtime_error("Invalid TX input index '" + strInIdx + "'");
     }
 


Test patch:
diff --git a/test/util/data/bitcoin-util-test.json b/test/util/data/bitcoin-util-test.json
index c9c64274c62af..83b3c430d5364 100644
--- a/test/util/data/bitcoin-util-test.json
+++ b/test/util/data/bitcoin-util-test.json
@@ -163,7 +163,85 @@
      "replaceable=0foo"],
     "return_code": 1,
     "error_txt": "error: Invalid TX input index",
-    "description": "Tests the check for an invalid input index with replaceable"
+    "description": "Tests the check for an invalid string input index with replaceable"
+  },
+  {
+    "exec": "./bitcoin-tx",
+    "args":
+    ["-create",
+     "in=5897de6bd6027a475eadd57019d4e6872c396d0716c4875a5f1a6fcfdf385c1f:0",
+     "replaceable=-1"],
+    "return_code": 1,
+    "error_txt": "error: Invalid TX input index",
+    "description": "Tests the check for an invalid negative input index with replaceable"
+  },
+  {
+    "exec": "./bitcoin-tx",
+    "args":
+    ["-create",
+     "in=5897de6bd6027a475eadd57019d4e6872c396d0716c4875a5f1a6fcfdf385c1f:0",
+     "replaceable=1"],
+    "return_code": 1,
+    "error_txt": "error: Invalid TX input index",
+    "description": "Tests the check for an invalid positive out-of-bounds input index with replaceable"
+  },
+  {
+    "exec": "./bitcoin-tx",
+    "args":
+    ["-create",
+     "in=5897de6bd6027a475eadd57019d4e6872c396d0716c4875a5f1a6fcfdf385c1f:0",
+     "replaceable=0"],
+    "output_cmp": "txreplacesingleinput.hex",
+    "description": "Tests that the 'SEQUENCE' value for a single input is set to fdffffff for single input"
+  },
+  {
+    "exec": "./bitcoin-tx",
+    "args":
+    ["-create",
+     "in=5897de6bd6027a475eadd57019d4e6872c396d0716c4875a5f1a6fcfdf385c1f:0",
+     "replaceable="],
+    "output_cmp": "txreplacesingleinput.hex",
+    "description": "Tests that the 'SEQUENCE' value for a single input is set to fdffffff when N omitted"
+  },
+  {
+    "exec": "./bitcoin-tx",
+    "args":
+    ["-create",
+     "in=5897de6bd6027a475eadd57019d4e6872c396d0716c4875a5f1a6fcfdf385c1f:0",
+     "in=bf829c6bcf84579331337659d31f89dfd138f7f7785802d5501c92333145ca7c:18",
+     "in=22a6f904655d53ae2ff70e701a0bbd90aa3975c0f40bfc6cc996a9049e31cdfc:1",
+     "replaceable=1"],
+    "output_cmp": "txreplace1.hex",
+    "description": "Tests that only the 'SEQUENCE' value of input[1] is set to fdffffff"
+  },
+  {
+    "exec": "./bitcoin-tx",
+    "args":
+    ["-create",
+     "in=5897de6bd6027a475eadd57019d4e6872c396d0716c4875a5f1a6fcfdf385c1f:0",
+     "in=bf829c6bcf84579331337659d31f89dfd138f7f7785802d5501c92333145ca7c:18",
+     "in=22a6f904655d53ae2ff70e701a0bbd90aa3975c0f40bfc6cc996a9049e31cdfc:1",
+     "replaceable="],
+    "output_cmp": "txreplaceomittedn.hex",
+    "description": "Tests that the 'SEQUENCE' value for each input is set to fdffffff when N omitted"
+  },
+  {
+    "exec": "./bitcoin-tx",
+    "args":
+    ["-create",
+     "replaceable="],
+    "output_cmp": "txreplacenoinputs.hex",
+    "description": "Tests behavior when no inputs are provided in the transaction"
+  },
+  {
+    "exec": "./bitcoin-tx",
+    "args":
+    ["-create",
+     "in=5897de6bd6027a475eadd57019d4e6872c396d0716c4875a5f1a6fcfdf385c1f:0:abcdef00",
+     "replaceable="],
+    "return_code": 1,
+    "error_txt": "error: invalid TX sequence id 'abcdef00'",
+    "description": "Try to make invalid input replaceable"
   },
   { "exec": "./bitcoin-tx",
     "args":
diff --git a/test/util/data/txreplace1.hex b/test/util/data/txreplace1.hex
new file mode 100644
index 0000000000000..7401c4e5dd475
--- /dev/null
+++ b/test/util/data/txreplace1.hex
@@ -0,0 +1,1 @@
+02000000031f5c38dfcf6f1a5f5a87c416076d392c87e6d41970d5ad5e477a02d66bde97580000000000ffffffff7cca453133921c50d5025878f7f738d1df891fd359763331935784cf6b9c82bf1200000000fdfffffffccd319e04a996c96cfc0bf4c07539aa90bd0b1a700ef72fae535d6504f9a6220100000000ffffffff0000000000
diff --git a/test/util/data/txreplacenoinputs.hex b/test/util/data/txreplacenoinputs.hex
new file mode 100644
index 0000000000000..22d830eda151e
--- /dev/null
+++ b/test/util/data/txreplacenoinputs.hex
@@ -0,0 +1,1 @@
+02000000000000000000
diff --git a/test/util/data/txreplaceomittedn.hex b/test/util/data/txreplaceomittedn.hex
new file mode 100644
index 0000000000000..a687836a099b5
--- /dev/null
+++ b/test/util/data/txreplaceomittedn.hex
@@ -0,0 +1,1 @@
+02000000031f5c38dfcf6f1a5f5a87c416076d392c87e6d41970d5ad5e477a02d66bde97580000000000fdffffff7cca453133921c50d5025878f7f738d1df891fd359763331935784cf6b9c82bf1200000000fdfffffffccd319e04a996c96cfc0bf4c07539aa90bd0b1a700ef72fae535d6504f9a6220100000000fdffffff0000000000
diff --git a/test/util/data/txreplacesingleinput.hex b/test/util/data/txreplacesingleinput.hex
new file mode 100644
index 0000000000000..b3e442795ed9a
--- /dev/null
+++ b/test/util/data/txreplacesingleinput.hex
@@ -0,0 +1,1 @@
+02000000011f5c38dfcf6f1a5f5a87c416076d392c87e6d41970d5ad5e477a02d66bde97580000000000fdffffff0000000000