Skip to content

commands: accept script(...) outputs in payto/paytomany, like the GUI - #11007

Open
devdavidejesus wants to merge 3 commits into
spesmilo:masterfrom
devdavidejesus:feat-4547-script-outputs
Open

devdavidejesus wants to merge 3 commits into
spesmilo:masterfrom
devdavidejesus:feat-4547-script-outputs

Conversation

@devdavidejesus

@devdavidejesus devdavidejesus commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #4547. Picks up #7203, which was closed as stale.

PaymentIdentifier, which the GUI uses to parse payees, already accepts arbitrary output scripts written as script(...), e.g. script(OP_RETURN 48656c6c6f). On the command line, payto/paytomany only resolved addresses, contacts and aliases. Now they accept the same syntax: the script(...) parsing moved into a shared parse_script() in payment_identifier.py (written by @f321x in review), used by both the GUI and the CLI. It strips surrounding whitespace and rejects an empty script().

$ electrum payto "script(OP_RETURN 48656c6c6f)" 0

Following the review on #7203: only script parsing is exposed (no openalias), and the existing payto-to-address test still passes. Tests: test_payto_script_output in tests/test_commands.py (OP_RETURN via payto, mixed with an address via paytomany, surrounding whitespace, and malformed or empty scripts, which raise a UserFacingException), and test_parse_script in tests/test_payment_identifier.py.

Also checked end to end on regtest with Bitcoin Core v31.1.0, with the first version of this PR: the transaction was accepted to the mempool and mined, and its nulldata output decodes to Hello. The shared parse_script() produces the same output script for this example (6a0548656c6c6f).

Coins sent to an OP_RETURN output are unspendable. With script(...) outputs,
'payto "script(OP_RETURN ...)" !' would put the whole wallet balance into
the OP_RETURN output, and the CLI has no confirmation dialog like the GUI.
Like bitcoind, whose "data" outputs always have zero value, require the
amount of an OP_RETURN output to be 0. Other scripts can still carry value.
@devdavidejesus

Copy link
Copy Markdown
Contributor Author

Added a guard: the amount of an OP_RETURN output must be 0, like bitcoind's "data" outputs. Otherwise payto "script(OP_RETURN ...)" ! would burn the whole wallet balance, and the CLI has no confirmation dialog like the GUI. Other scripts can still carry value. The malformed-script test now also checks the error message.

Comment thread electrum/commands.py Outdated
scriptpubkey = PaymentIdentifier.parse_script(m.group(1))
except Exception as e:
raise UserFacingException(f"Invalid script: {m.group(1)!r}") from e
if scriptpubkey[:1] == bytes([opcodes.OP_RETURN]) and amount_sat != 0:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the user wants to burn coins? Not sure if we want to disallow this, if the user pays to scripts they should know how it works, and the Qt GUI allows doing it too.
Note by default Bitcoin Core will reject these transactions anyway when trying to broadcast, see maxburnamount in https://bitcoincore.org/en/doc/31.0.0/rpc/rawtransactions/sendrawtransaction/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO non-zero amounts for OP_RETURN on the CLI should be allowed only iff --iknowwhatimdoing is passed

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we add an additional iknowwhatimdoing parameter i propose just gating the whole script output functionality behind it. There are other ways beside OP_RETURN scripts that allow burning coins too.

But still in the desktop GUI we allow all of this without any special treatment (and i guess it is realistic to assume CLI users being more technical than GUI users).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looked into this a bit more before answering (my earlier comment overstated the risk):

  • payto only creates and signs the transaction; nothing is burned until broadcast, which goes through the Electrum server.
  • Whether it gets rejected there depends on the server: ElectrumX and electrs call sendrawtransaction with only the raw tx, so bitcoind's default maxburnamount=0 applies, but Fulcrum passes maxburnamount=21000000 (Servers.cpp#L2037), so there it goes through.
  • As @f321x said, an OP_RETURN-only gate is easy to get around: script(OP_0) with a value is accepted too, and it is just as unspendable. Gating all script(...) outputs would also hit zero-value OP_RETURN outputs.

So for this PR I'd keep payto like the GUI (pushed that way). If we want a guard, I think it belongs at broadcast time, in Electrum itself and for both the CLI and the GUI, like bitcoind's maxburnamount, so that it doesn't depend on the server. I can do that in a separate PR if you agree.

Comment thread electrum/commands.py Outdated
As suggested in review: use a shared parse_script() in payment_identifier.py
for both the GUI and the CLI. It strips surrounding whitespace, like the
GUI, and rejects an empty script(), which used to create an output with an
empty scriptPubKey. The OP_RETURN amount check is dropped: the Qt GUI
allows sending value to an OP_RETURN output, and bitcoind's
sendrawtransaction rejects it by default (maxburnamount=0).

Co-authored-by: f321x <f@f321x.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RPC doesn't support "OP_RETURN <data hex bytes>" feature of GUI

3 participants