diff --git a/changelog.d/sendraw-allowhighfees.md b/changelog.d/sendraw-allowhighfees.md new file mode 100644 index 0000000000..a5fe4f624e --- /dev/null +++ b/changelog.d/sendraw-allowhighfees.md @@ -0,0 +1,6 @@ +Fixed + +- **LND and other btcd `rpcclient` users can broadcast.** That client reads + `/rbitcoin:` as Core before 0.19 and sends `sendrawtransaction` a boolean + `allowhighfees`, which was `-3 maxfeerate is not a number or string`. + `true` now lifts the fee cap and `false` keeps the default 10000 sat/vB. diff --git a/crates/rbitcoin-rpc/src/methods/mempool.rs b/crates/rbitcoin-rpc/src/methods/mempool.rs index 14b3ea6180..ef8b731e1e 100644 --- a/crates/rbitcoin-rpc/src/methods/mempool.rs +++ b/crates/rbitcoin-rpc/src/methods/mempool.rs @@ -646,7 +646,13 @@ pub const MAX_BURN_MSG: &str = pub fn sendrawtransaction(ctx: &RpcContext, params: &RpcParams) -> Result { params.reject_unknown(&["hexstring", "maxfeerate", "maxburnamount"])?; let hex = params.req_str(0, "hexstring")?; - let max_feerate = opt_maxfeerate_sat_vb(params, 1)?; + // Pre-0.19 `allowhighfees`. btcd rpcclient (LND, btcwallet) sends it + // because `/rbitcoin:` sorts below its `0.19.0` subversion check. + let max_feerate = match params.get(1, "maxfeerate") { + Some(Value::Bool(true)) => 0, + Some(Value::Bool(false)) => DEFAULT_MAX_RAW_TX_FEE_SAT_VB, + _ => opt_maxfeerate_sat_vb(params, 1)?, + }; let max_burn = opt_maxburn_sat(params, 2)?; let tx = decode_tx_hex(hex)?; let mp = ctx diff --git a/crates/rbitcoin-rpc/src/methods/mod.rs b/crates/rbitcoin-rpc/src/methods/mod.rs index 24e00cc47e..8f92bf586c 100644 --- a/crates/rbitcoin-rpc/src/methods/mod.rs +++ b/crates/rbitcoin-rpc/src/methods/mod.rs @@ -862,6 +862,7 @@ pub const NAMED_HELP: &[(&str, &str)] = &[ "sendrawtransaction", "sendrawtransaction hexstring (maxfeerate) (maxburnamount)\n\ RPC submit only. Default maxfeerate 10000 sat/vB (0 unlimited; >=100000 sat/vB is a parameter error). \ + A pre-0.19 boolean allowhighfees there: true is unlimited, false the default. \ Default maxburnamount 0. P2P relay is not capped.", ), ( diff --git a/crates/rbitcoin-rpc/tests/methods/mod.rs b/crates/rbitcoin-rpc/tests/methods/mod.rs index 453ac584ea..677f4ee74f 100644 --- a/crates/rbitcoin-rpc/tests/methods/mod.rs +++ b/crates/rbitcoin-rpc/tests/methods/mod.rs @@ -2404,11 +2404,25 @@ fn pin_sendraw_maxfeerate_at_default_and_one_sat_over(ctx: &RpcContext, at: u32, cb - max_fee - 1, ScriptBuf::from_bytes(vec![0x51]), ); - let e = dispatch(ctx, "sendrawtransaction", vec![json!(over_hex)]).unwrap_err(); - assert_eq!(e["code"], ERR_VERIFY_ERROR, "{e}"); - assert_eq!( - e["message"], "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", - "{e}" + // Pre-0.19 `allowhighfees`, which btcd rpcclient sends for a `/rbitcoin:` + // subversion: `false` keeps the default cap, `true` lifts it. + for params in [vec![json!(over_hex)], vec![json!(over_hex), json!(false)]] { + let e = dispatch(ctx, "sendrawtransaction", params).unwrap_err(); + assert_eq!(e["code"], ERR_VERIFY_ERROR, "{e}"); + assert_eq!( + e["message"], "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", + "{e}" + ); + } + let ok = dispatch( + ctx, + "sendrawtransaction", + vec![json!(over_hex), json!(true)], + ) + .unwrap(); + assert!( + ok.as_str().is_some(), + "allowhighfees true must lift the cap: {ok}" ); } @@ -2427,7 +2441,7 @@ fn pin_maxfeerate_json_shapes(ctx: &RpcContext) { "maxfeerate must be an integer sat/vB", ), ( - json!(true), + json!({}), ERR_TYPE_ERROR, "maxfeerate is not a number or string", ), diff --git a/docs/lightning.md b/docs/lightning.md index dd53144e68..101a5b9377 100644 --- a/docs/lightning.md +++ b/docs/lightning.md @@ -243,6 +243,13 @@ when that integer is at least `150000`, which it is. Raising the integer toward a current Core release would advertise wallet RPC that this node does not have. See [`rpc.md`](./rpc.md#getnetworkinfoversion). +LND reaches this node through btcd `rpcclient`, which string-compares +`subversion` after trimming `/Satoshi:`. `/rbitcoin:…/` sorts below +`0.19.0`, so the client speaks the pre-0.19 dialect: +`sendrawtransaction` gets the boolean `allowhighfees`, which this node +accepts. The client also refuses to call `testmempoolaccept`, so LND +skips that pre-check and publishes directly. + `estimatesmartfee` accepts `estimate_mode` `economical` and `conservative` and returns the same 10-minute feerate either way. `initialblockdownload` means relay is inhibited (`--min-chain-work` / `--max-tip-age`), not diff --git a/docs/rpc.md b/docs/rpc.md index a192a373f6..11cc3ca3e2 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -144,7 +144,7 @@ still wait for durable SH when shindex is on. | `decoderawtransaction` | All networks. Decode hex. Optional `iswitness`: `false` refuses a BIP141 marker (`-22 TX decode failed`). Extra trailing bytes also `-22`. `scriptSig.asm` is rust-bitcoin, not Core `ScriptToAsmStr` sighash suffixes. Coinbase vin is Core-shaped `{coinbase, sequence}` (plus rbitcoin's `n`), with no `txid`/`vout`. | | `decodescript` | All networks. `asm`, Core-style `type`, `hex`, `address` when `Address::from_script` succeeds, and `desc`. `desc` is Core's inference with no keystore: `pk`, `multi`, `rawtr`, `addr`, else `raw`, with the BIP380 checksum; `pkh` / `wpkh` / `sh` / `wsh` / `tr` never appear. No `p2sh` wrap, `segwit` wrap, or miniscript. | | `validateaddress` | All networks. Valid: `isvalid`, `address`, `scriptPubKey`, `isscript`, `iswitness`, plus `witness_version` / `witness_program` when segwit except pay-to-anchor. P2TR and P2A set `isscript` true. Invalid (parse fail or wrong chain): `{isvalid: false}` only — no `error` / `error_locations`. | -| `sendrawtransaction` / `testmempoolaccept` | `sendrawtransaction` is live accept (RPC still admits under `-blocksonly`; serving-only refuse is IBD / tip-not-ready `relay disabled`). `testmempoolaccept` / `submitpackage` `vsize` is sigop-adjusted. `testmempoolaccept` is dry-run (`MempoolHub::test_accept`: prepare + scripts + RBF/cluster checks, no commit / announce / RBF eviction / orphan park). Invalid hex / decode is `-22 TX decode failed` (same as `decoderawtransaction`). Confirmed on the active chain is `txn-already-known` (tip + confirmed-strong); live mempool duplicate is `txn-already-in-mempool`; archive-only after invalidate is not already-known. RPC-submit only: `maxfeerate` is **sat/vB** (default **10000**; `0` unlimited; `>= 100000` is `-8`). sendraw over-cap is `-25` configured-max text; `testmempoolaccept` reject-reason stays `max-fee-exceeded`. `maxburnamount` default **0** (valued unspendable / OP_RETURN outs). nVersion outside 1/2 is `"version"`. P2P `accept_tx` does not apply these caps. Multi-tx `testmempoolaccept` is sequential: earlier `allowed: true` rows stay (no abort-class blanking). Core functional tests still speak BTC/kvB via `scripts/core-functional/rpc_proxy.py`. | +| `sendrawtransaction` / `testmempoolaccept` | `sendrawtransaction` is live accept (RPC still admits under `-blocksonly`; serving-only refuse is IBD / tip-not-ready `relay disabled`). `testmempoolaccept` / `submitpackage` `vsize` is sigop-adjusted. `testmempoolaccept` is dry-run (`MempoolHub::test_accept`: prepare + scripts + RBF/cluster checks, no commit / announce / RBF eviction / orphan park). Invalid hex / decode is `-22 TX decode failed` (same as `decoderawtransaction`). Confirmed on the active chain is `txn-already-known` (tip + confirmed-strong); live mempool duplicate is `txn-already-in-mempool`; archive-only after invalidate is not already-known. RPC-submit only: `maxfeerate` is **sat/vB** (default **10000**; `0` unlimited; `>= 100000` is `-8`). `sendrawtransaction` also takes the pre-0.19 boolean `allowhighfees` in that slot (`true` unlimited, `false` the default); btcd `rpcclient` sends it because `/rbitcoin:` sorts below its `0.19.0` subversion check. sendraw over-cap is `-25` configured-max text; `testmempoolaccept` reject-reason stays `max-fee-exceeded`. `maxburnamount` default **0** (valued unspendable / OP_RETURN outs). nVersion outside 1/2 is `"version"`. P2P `accept_tx` does not apply these caps. Multi-tx `testmempoolaccept` is sequential: earlier `allowed: true` rows stay (no abort-class blanking). Core functional tests still speak BTC/kvB via `scripts/core-functional/rpc_proxy.py`. | | `estimatesmartfee` | **10-minute inclusion frontier** — not Core historical multi-horizon. Core's result shape: `{feerate, blocks}`, or `{errors, blocks}` with no `feerate` when there is no estimate. Like Core, `feerate` is at least `mempoolminfee`. See [`mempool-fee-estimation.md`](./mempool-fee-estimation.md). | | `estimaterawfee` | Same 10-minute frontier product as `estimatesmartfee` (Core RPC name for harness scripts). Not Core historical `estimaterawfee` buckets. | | `getnetworkhashps` | Core `GetNetworkHashPS`: `chainwork(end) − chainwork(start)` over `(maxTime − minTime)` in the lookup window. Default `nblocks` 120; `nblocks<=0` uses `height % difficulty_adjustment_interval + 1` (capped to height). `height<0` or past tip → tip. Genesis / zero dt → `0.0`. |