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:
-
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.
-
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)
Summary
blockchain.transaction.get(txid, verbose=true)is intended to return Core'sgetrawtransactionshape (the source even acknowledges this atsrc/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:hash"0000...0000"for witness coinbasesizevout[i].scriptPubKey.type"nonstandard"for textbook standard outputs"pubkeyhash","scripthash","witness_v0_keyhash", etc.vout[i].scriptPubKey.addressvout[i].scriptPubKey.asm[bracketed]push datavout[i].scriptPubKey.addressesAll four live in the same
bitcoind(*tx)JSON-builder code path, called fromhandle_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) callsclient.request('blockchain.transaction.get', txid, true)for every tx in the wallet's history. ItsparseAddressesfunction:Tested against bs's response for testnet3 tx
09afbaf6...0b(h=1,000,000):"3fa0b7dc3bb7737a9ccb170df1d1d6a4f32f290b18d92826f3dfe77cbe2dd033""mzrj4QmPhk98vc2yQw42uCsgwfBjVzPPLM""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 rawhexlocally for vout/scriptPubKey rendering. So Sparrow IS unaffected by ECH-37 (scriptPubKeydeviations) — but it DOES consumehashandsize, both of which bs returns wrong for witness txs:tx.hash0000...0000461091484b8fb4f9e8a9...4bb9tx.size176(stripped)212(witness-included)Sparrow uses
sizefor sat/vB fee-rate display — a ~14-25% inflated fee rate for witness txs when pointed at bs.Electrum (the wallet,
spesmilo/electrum) never sendsverbose=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:Self-validating — the expected value is computed from
value_from(bitcoind(coinbase)), the same buggy function under test. Ifbitcoind()puts"type": "nonstandard"in the response, the assertion'sexpectedalso says"nonstandard". They match. Test passes. Bug undetected.Pre-segwit-only fixture — the test uses
test::genesis(Bitcoin's genesis coinbase). That's non-witness, sohash == txidandstripped == fullby 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 thebitcoind()helper internally appears to use the legacy (pre-witness) values forhashandsize. Fix: inbitcoind(), computehashas the wtxid and useserialized_size(true)for thesizefield. Probably 1-3 lines each.Root cause 2: missing Core-style address decoder (covers ECH-37, ECH-43). To emit
type/addressfields 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 theaddressdecoder is in place, the misnamedaddressesfield (ECH-43) can be removed.The
asmformatter (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
echennells/libbitcoin-test-infra/tests/electrum/runner.py+cases.ymltests/electrum/reports/run-20260528T035602Z/results.json1c3908c8(libbitcoin-server)