Skip to content

Esplora tx JSON: witness: [] on non-witness inputs, and inner_redeemscript_asm/inner_witnessscript_asm on the wrong inputs (P2PKH, coinbase, P2WPKH, P2TR control block) #852

Description

@nullcount

Version: observed on aa6de36c (0.7.99), mainnet, Esplora over the unix socket. crates/rbitcoin-esplora/src/tx_json.rs is identical on origin/master 1aece752.

Related: #814 (mempool support), #632 (0.8 electrs drop-in). COMPAT.md describes the 0.8 goal as "drop-in mempool/electrs or Blockstream electrs HTTP". None of the fields below is listed there as a deliberate difference.

What happens

For every input, /tx/:txid (and every route that builds tx JSON: /block/:h/txs, /address/:a/txs, …) does the following:

  1. It always emits "witness": [...], including "witness": [] for inputs that have no witness. electrs leaves the key out.
  2. It emits inner_redeemscript_asm whenever the scriptSig's last push is non-empty, whatever the prevout type, by decoding that push as a script. For a P2PKH input this is the pubkey, and for a coinbase it is the coinbase data. electrs emits it only when the prevout is P2SH.
  3. It emits inner_witnessscript_asm whenever the witness has 2 or more items and the last item does not start with 0x30, by decoding the last item as a script:
    • P2WPKH and P2SH-P2WPKH: the pubkey is decoded as a script. electrs emits nothing.
    • P2TR script path: the control block (last item) is decoded. mempool/electrs decodes the tapscript, which is the second-to-last item, or third-to-last when there is an annex. Blockstream electrs emits nothing for P2TR.

The output is garbage asm such as OP_PUSHBYTES_40 <push past end>, and it breaks byte-for-byte comparison with electrs. mempool's frontend shows these fields in the tx details view.

Evidence

Our node vs mempool.space, same txids. prevout is removed for brevity, and long strings are cut with … by jq.

P2PKH input: 6d2a0f0eb72eb8f87946b6f7344f70197ee0a5b78743b4c9f26c882301d8e944 vin 0.

$ curl -s --unix-socket /run/rbitcoin/esplora.sock http://x/tx/6d2a0f0e…e944 | jq -c '.vin[0] | del(.prevout)'
{"inner_redeemscript_asm":"OP_PUSHBYTES_3 f2fd4e OP_EQUAL OP_PUSHBYTES_40 <push past end>","is_coinbase":false,"scriptsig":"47304402…","scriptsig_asm":"OP_PUSHBYTES_71 304402…","sequence":4294967295,"txid":"c8c1a80b…","vout":4,"witness":[]}

$ curl -s https://mempool.space/api/tx/6d2a0f0e…e944 | jq -c '.vin[0] | keys'
["is_coinbase","prevout","scriptsig","scriptsig_asm","sequence","txid","vout"]

P2WPKH input: 1da61ed9c7323d42bc8355c165ab94688aa7d86ec23a49f8834b31736b299e58 vin 0.

rbitcoin:     "inner_witnessscript_asm": "OP_PUSHBYTES_2 c3fb OP_RETURN_236 OP_RETURN_219 OP_RESERVED1 OP_PUSHBYTES_2 5cf6 OP_PUSHBYTES_40 <push past end>"
mempool.space: keys = ["is_coinbase","prevout","scriptsig","scriptsig_asm","sequence","txid","vout","witness"]   (no inner_* key)

P2SH-P2WPKH input: 2d8e0faae34eb69c3fed808acb99071d988ec2dca0514ac659bf8d6fddbb11c3 vin 0. inner_redeemscript_asm (OP_0 OP_PUSHBYTES_20 29ee…) is correct, but rbitcoin also emits

"inner_witnessscript_asm": "OP_PUSHBYTES_3 3c643f OP_RETURN_194 OP_PUSHBYTES_6 e1f4d06adc46 … OP_PUSHBYTES_64 <push past end>"

Coinbase: 87ee4721c4d918ae883b20aff82b8a645ecad7ff526aa6e474939a0def642c83 (height 969500).

{"is_coinbase":true,"inner_redeemscript_asm":"OP_PUSHBYTES_52 <push past end>","witness":["0000…0000"]}

P2TR script path: the first inscription reveal, 6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799, witness [sig, tapscript, control block].

rbitcoin:      "inner_witnessscript_asm": "OP_RETURN_192 OP_PUSHBYTES_74 <push past end>"        # control block c04a3c… decoded
mempool.space: "inner_witnessscript_asm": "OP_PUSHBYTES_32 4a3ca2cf…0cc5 OP_CHECKSIG OP_0 OP_IF OP_PUSHBYTES_3 6f7264 OP_PUSHBYTES_1 01 OP_PUSHBYTES_9 696d6167652f706e67 OP_0 OP_PUSHDATA2 8950…"

We compared 487 requests between a mempool instance backed by rbitcoin and one backed by Core+electrs. 82 of the 104 mismatches were Esplora-shape differences: these fields, plus the paging, --max-sh-creates and summary differences filed separately.

Source

crates/rbitcoin-esplora/src/tx_json.rs at 1aece752 (same lines at aa6de36):

  • :311-312: vin_obj["witness"] = json!(wit); runs unconditionally.
  • :314-319: both inner_* helpers are called without looking at the prevout script type, although the prevout is resolved a few lines later (prevout_json, :323).
  • :444-450: inner_redeemscript_asm decodes the last scriptSig push.
  • :457-470: inner_witnessscript_asm decodes the last witness item unless it starts with 0x30. It has no P2TR handling.

Reference:

  • Blockstream electrs new-index @ cb7c72f9, src/rest.rs:218-249: witness is Option with skip_serializing_if, and is None when the witness is empty.
  • Blockstream electrs src/util/script.rs:44-81 (get_innerscripts): redeem script only if prevout.is_p2sh(), witness script only if the prevout is P2WSH or the redeem script is P2WSH.
  • mempool/electrs mempool @ 9b14438a, src/util/script.rs:141-187: adds P2TR, taking the second-to-last witness item, or third-to-last when the last item starts with 0x50 (annex).

Suggested fix

Decide on the prevout's script_pubkey, which is already available as prev_spks/prevout_json:

  • witness: omit the key when tin.witness.is_empty(). Note that the pruned-path tests at tx_json.rs:951,958 already assert that the key is absent there.
  • inner_redeemscript_asm: only when the prevout is P2SH.
  • inner_witnessscript_asm: when the prevout is P2WSH, or P2SH whose redeem script is P2WSH: the last witness item. When the prevout is P2TR with at least 2 items after stripping the annex: the second-to-last item, as mempool/electrs does.

For a mempool-only input with an unknown prevout, electrs emits neither field.

Workaround

Clients have to normalise; our own backend strips these fields before re-serving them. Anything reading rbitcoin's Esplora directly (mempool's /api/tx passthrough, wallets, explorers) sees the extra fields.

Activity

  1. rearden-grok commented on Oct 4, 2026

    @rearden-grok
    Contributor

    Fixed in #861 (merge 549e42737), which is on master.

    Tx JSON omits witness when the witness is empty. inner_redeemscript_asm is only the P2SH redeem script. inner_witnessscript_asm is only a P2WSH witness script (including P2SH-P2WSH) or a Taproot script-path leaf. A trailing annex (0x50) is stripped first, so the control block is not decoded as the script. A P2PKH input, a coinbase, a P2WPKH input, an unknown prevout, and a Taproot key-path spend emit neither inner field.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions