Skip to content

[Bug] P2SH-P2WPKH (nested-segwit) lightweight send assigns raw bytes to script_sig instead of a Script - final_tx.serialize() raises, so a nested-segwit miner can never broadcast a destination payout #470

@JSONbored

Description

@JSONbored

Summary

In the lightweight (embit) send path, the finalize step sets final_tx.vin[i].script_sig to segwit_script.serialize()raw bytes — for the p2sh-p2wpkh case, while the legacy-P2PKH branch seven lines below correctly wraps its scriptSig in Script(...). embit's TransactionInput.script_sig must be a Script (serialization calls script_sig.serialize()), so final_tx.serialize() raises AttributeError: 'bytes' object has no attribute 'serialize', which is caught and the send returns None. A miner whose committed BTC address is P2SH-P2WPKH (3… mainnet / 2… testnet) in BTC_MODE=lightweight therefore cannot broadcast any destination payout — every fulfillment fails, and the miner is slashed on every swap it takes.

Affected code (branch test, e360977)

allways/chain_providers/bitcoin.py:721-735 — the same field assigned two different types in adjacent branches of the same loop:

for i, inp in enumerate(psbt.inputs):
    if is_segwit:
        for pub, sig in inp.partial_sigs.items():
            final_tx.vin[i].witness = Witness([sig, pub.sec()])
            if addr_type == 'p2sh-p2wpkh':
                final_tx.vin[i].script_sig = segwit_script.serialize()   # <-- raw bytes (BUG)
    else:
        from embit.script import Script
        for pub, sig in inp.partial_sigs.items():
            ...
            final_tx.vin[i].script_sig = Script(                          # <-- correctly a Script
                bytes([len(sig_bytes)]) + sig_bytes + bytes([len(pub_bytes)]) + pub_bytes
            )

raw_tx = final_tx.serialize().hex()   # :737 — calls each vin.script_sig.serialize()

P2SH-P2WPKH is a supported address type: type_to_script maps it (bitcoin.py:647), is_segwit includes it (:663), and the redeem-script is set on the PSBT input (:693-695). On the send-failure path, the AttributeError is swallowed and recorded as a generic send error (return None).

Proof

segwit_script.serialize() returns bytes; embit's TransactionInput.script_sig must be a Script object, and Transaction.serialize() calls script_sig.serialize()bytes has no .serialize(), so the call raises. The legacy branch four lines down constructs Script(<scriptSig bytes>), which is the correct type — the two branches cannot both be right for the same field. (Verified empirically against the repo's locked embit: wrapping as Script(segwit_script.serialize()) serializes to a valid nested-segwit scriptSig 160014<20-byte-keyhash> and a stable txid; the raw-bytes form raises.) Native P2WPKH and legacy P2PKH are unaffected — which is why it slipped through.

Impact

100% broadcast failure for any nested-segwit miner running BTC_MODE=lightweight: it can never mark a swap fulfilled, so it is timeout-slashed on every swap it reserves — full, ongoing earnings loss for that miner. Missed because the existing tests cover BIP-137 message signing only, never transaction construction for this address type.

Suggested fix

final_tx.vin[i].script_sig = Script(segwit_script.serialize())

(import Script as the legacy branch already does). Add a serialization test for the nested-segwit finalize path (addr_type == 'p2sh-p2wpkh') asserting final_tx.serialize() succeeds and produces the 160014… scriptSig.


Verified against test / main @ e360977. git log -S 'script_sig = segwit_script.serialize()' confirms this line is unchanged since the initial commit — never touched, despite this exact finalize block being actively maintained: PR #279 (merged) fixed an adjacent embit mutable-default leak here, #267 added the Esplora fallback, #225 added the traceback logging that now surfaces this very failure — none touched the script_sig type. Distinct from #459 (open — P2SH-P2WPKH fee-sizing, which assumes this send path works), #448/#460/#453 (address validation / Taproot / network), and #33/#34 (testnet P2SH classification). No open or merged PR/issue addresses the send-path script_sig type.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions