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.
Summary
In the lightweight (
embit) send path, the finalize step setsfinal_tx.vin[i].script_sigtosegwit_script.serialize()— rawbytes— for thep2sh-p2wpkhcase, while the legacy-P2PKH branch seven lines below correctly wraps its scriptSig inScript(...). embit'sTransactionInput.script_sigmust be aScript(serialization callsscript_sig.serialize()), sofinal_tx.serialize()raisesAttributeError: 'bytes' object has no attribute 'serialize', which is caught and the send returnsNone. A miner whose committed BTC address is P2SH-P2WPKH (3…mainnet /2…testnet) inBTC_MODE=lightweighttherefore 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:P2SH-P2WPKH is a supported address type:
type_to_scriptmaps it (bitcoin.py:647),is_segwitincludes it (:663), and the redeem-script is set on the PSBT input (:693-695). On the send-failure path, theAttributeErroris swallowed and recorded as a generic send error (return None).Proof
segwit_script.serialize()returnsbytes;embit'sTransactionInput.script_sigmust be aScriptobject, andTransaction.serialize()callsscript_sig.serialize()—byteshas no.serialize(), so the call raises. The legacy branch four lines down constructsScript(<scriptSig bytes>), which is the correct type — the two branches cannot both be right for the same field. (Verified empirically against the repo's lockedembit: wrapping asScript(segwit_script.serialize())serializes to a valid nested-segwit scriptSig160014<20-byte-keyhash>and a stable txid; the raw-bytesform 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
(import
Scriptas the legacy branch already does). Add a serialization test for the nested-segwit finalize path (addr_type == 'p2sh-p2wpkh') assertingfinal_tx.serialize()succeeds and produces the160014…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 exactfinalizeblock being actively maintained: PR #279 (merged) fixed an adjacentembitmutable-default leak here, #267 added the Esplora fallback, #225 added the traceback logging that now surfaces this very failure — none touched thescript_sigtype. 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-pathscript_sigtype.