Skip to content

Verbose blockchain.transaction.get response deviates from Core's getrawtransaction shape in 4 fields #1

Description

@echennells

Summary

blockchain.transaction.get(txid, verbose=true) is intended to return Core's getrawtransaction shape (the source even acknowledges this at src/protocols/electrum/protocol_electrum_transactions.cpp:217: // Verbose is whatever bitcoind returns for getrawtransaction, lolz.). The response currently deviates from that shape in 4 separate fields:

Linear Field bs returns Core / Fulcrum / electrs return
ECH-38 top-level hash "0000...0000" for witness coinbase actual wtxid
ECH-39 top-level size stripped (pre-segwit) serialization size full witness-included size
ECH-37 vout[i].scriptPubKey.type "nonstandard" for textbook standard outputs "pubkeyhash", "scripthash", "witness_v0_keyhash", etc.
ECH-37 vout[i].scriptPubKey.address (missing) base58check / bech32 / bech32m string
ECH-37 vout[i].scriptPubKey.asm lowercase, [bracketed] push data uppercase, space-separated
ECH-43 vout[i].scriptPubKey.addresses array containing an opaque 64-char hex hash (Core omits this deprecated field entirely)

All four live in the same bitcoind(*tx) JSON-builder code path, called from handle_blockchain_transaction_get (protocol_electrum_transactions.cpp:218).

Why this matters (real wallet impact, not theoretical)

Differential test against real wallet code:

Trezor Suite (packages/blockchain-link/src/workers/electrum/utils/transaction.ts, Apache-2.0) calls client.request('blockchain.transaction.get', txid, true) for every tx in the wallet's history. Its parseAddresses function:

const parseAddresses = ({ address, addresses, type, hex }) => {
    if (type === 'nulldata') { ... }                              // bs's "nonstandard" skips this branch
    const addrs = !address ? addresses || [] : [address];         // bs has no address, falls through to addresses
    return { addresses: addrs, isAddress: addrs.length === 1 };   // bs's hex hash treated as a real address
};

Tested against bs's response for testnet3 tx 09afbaf6...0b (h=1,000,000):

Output Trezor would display (bs) Trezor would display (Fulcrum/electrs)
vout[0] P2PKH "3fa0b7dc3bb7737a9ccb170df1d1d6a4f32f290b18d92826f3dfe77cbe2dd033" "mzrj4QmPhk98vc2yQw42uCsgwfBjVzPPLM"
vout[1] OP_RETURN "0e666c3fd703d717797c4fd2ce71c508b9bb243a3b6807b73b808b08d417f25c" "OP_RETURN (*!)mbv..." (decoded payload)

A Trezor user with a bs-backed Electrum server would see opaque 64-char hex strings in their wallet's history view in place of every recipient address. That's user-visible data corruption.

Sparrow Wallet (net/VerboseTransaction.java, GPL-3.0) reads 9 fields from the verbose response and decodes the raw hex locally for vout/scriptPubKey rendering. So Sparrow IS unaffected by ECH-37 (scriptPubKey deviations) — but it DOES consume hash and size, both of which bs returns wrong for witness txs:

Field bs stores Fulcrum (correct)
tx.hash 0000...0000 461091484b8fb4f9e8a9...4bb9
tx.size 176 (stripped) 212 (witness-included)

Sparrow uses size for sat/vB fee-rate display — a ~14-25% inflated fee rate for witness txs when pointed at bs.

Electrum (the wallet, spesmilo/electrum) never sends verbose=true — verified by grep across the repo. So this issue is invisible to Electrum-the-wallet users.

Why the existing test doesn't catch this

test/protocols/electrum/electrum_transactions.cpp:263 (electrum__blockchain_transaction_get__version_1_2_verbose__expected) is the only verbose-tx test. It has two structural problems:

  1. Self-validating — the expected value is computed from value_from(bitcoind(coinbase)), the same buggy function under test. If bitcoind() puts "type": "nonstandard" in the response, the assertion's expected also says "nonstandard". They match. Test passes. Bug undetected.

  2. Pre-segwit-only fixture — the test uses test::genesis (Bitcoin's genesis coinbase). That's non-witness, so hash == txid and stripped == full by definition. ECH-38 and ECH-39 wouldn't manifest even if the assertion were against external truth.

The differential rig at echennells/libbitcoin-test-infra/tests/electrum/ catches all 4 because it (a) uses real witness txs and (b) compares against Fulcrum and electrs, which both follow the Core-compat convention.

Possible fix approaches

Two root causes underneath the 4 surface symptoms:

Root cause 1: post-segwit retrofit gap (covers ECH-38, ECH-39). The outer verbose handler already passes serialized_size(true) for byte-counting, but the bitcoind() helper internally appears to use the legacy (pre-witness) values for hash and size. Fix: in bitcoind(), compute hash as the wtxid and use serialized_size(true) for the size field. Probably 1-3 lines each.

Root cause 2: missing Core-style address decoder (covers ECH-37, ECH-43). To emit type/address fields matching Core's classification, bitcoind() needs to pattern-match output scripts (pubkeyhash/scripthash/witness_v0_keyhash/witness_v0_scripthash/witness_v1_taproot/nulldata/pubkey/multisig/nonstandard) and emit the right base58check/bech32/bech32m address. Both the script-shape detection and the address encoders almost certainly already exist in libbitcoin (used by wallet tooling and validation). The work is wiring them into the verbose JSON builder. Probably ~100-300 lines including tests. Once the address decoder is in place, the misnamed addresses field (ECH-43) can be removed.

The asm formatter (lowercase → uppercase, brackets → spaces) is a separate few-line change.

For discussion

Open question: does the verbose endpoint want to faithfully mirror Core's getrawtransaction, or is the current shape acceptable as an alternative "implementation-specific" form? The source's "lolz" comment suggests the goal is Core-compat, but the half-finished state suggests the work was deprioritized.

Filing here on the fork rather than upstream as a placeholder so we can decide the approach before going to upstream.

Cross-references

  • Linear: ECH-37, ECH-38, ECH-39, ECH-43
  • Test rig: echennells/libbitcoin-test-infra/tests/electrum/runner.py + cases.yml
  • Run-003 report (testnet3 differential, 2026-05-28): tests/electrum/reports/run-20260528T035602Z/results.json
  • Mainnet cross-confirm at block 800000 coinbase, libbitcoinvm tailnet node, bs heads 1c3908c8 (libbitcoin-server)

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