async def step4_serialize_inputs(self) -> None: writers.write_varint(self.serialized_tx, self.tx.inputs_count) prefix_hash = self.h_prefix.get_digest() for i_sign in range(self.tx.inputs_count): progress.advance() txi_sign = await helpers.request_tx_input(self.tx_req, i_sign, self.coin) self.wallet_path.check_input(txi_sign) self.multisig_fingerprint.check_input(txi_sign) key_sign = self.keychain.derive(txi_sign.address_n) key_sign_pub = key_sign.public_key() if txi_sign.script_type == InputScriptType.SPENDMULTISIG: prev_pkscript = scripts.output_script_multisig( multisig.multisig_get_pubkeys(txi_sign.multisig), txi_sign.multisig.m, ) elif txi_sign.script_type == InputScriptType.SPENDADDRESS: prev_pkscript = scripts.output_script_p2pkh( addresses.ecdsa_hash_pubkey(key_sign_pub, self.coin) ) else: raise SigningError("Unsupported input script type") h_witness = self.create_hash_writer() writers.write_uint32( h_witness, self.tx.version | DECRED_SERIALIZE_WITNESS_SIGNING ) writers.write_varint(h_witness, self.tx.inputs_count) for ii in range(self.tx.inputs_count): if ii == i_sign: writers.write_bytes_prefixed(h_witness, prev_pkscript) else: writers.write_varint(h_witness, 0) witness_hash = writers.get_tx_hash( h_witness, double=self.coin.sign_hash_double, reverse=False ) h_sign = self.create_hash_writer() writers.write_uint32(h_sign, DECRED_SIGHASH_ALL) writers.write_bytes_fixed(h_sign, prefix_hash, writers.TX_HASH_SIZE) writers.write_bytes_fixed(h_sign, witness_hash, writers.TX_HASH_SIZE) sig_hash = writers.get_tx_hash(h_sign, double=self.coin.sign_hash_double) signature = ecdsa_sign(key_sign, sig_hash) # serialize input with correct signature gc.collect() script_sig = self.input_derive_script(txi_sign, key_sign_pub, signature) writers.write_tx_input_decred_witness( self.serialized_tx, txi_sign, script_sig ) self.set_serialized_signature(i_sign, signature)
async def step6_sign_segwit_inputs(self) -> None: any_segwit = bool(self.segwit) for i in range(self.tx.inputs_count): progress.advance() if i in self.segwit: await self.sign_segwit_input(i) elif any_segwit: # add empty witness for non-segwit inputs self.serialized_tx.append(0)
async def step1_process_inputs(self) -> None: for i in range(self.tx.inputs_count): # STAGE_REQUEST_1_INPUT in legacy progress.advance() txi = await helpers.request_tx_input(self.tx_req, i, self.coin) self.weight.add_input(txi) if input_is_segwit(txi): self.segwit.add(i) await self.process_input(txi)
async def step4_serialize_inputs(self) -> None: self.write_tx_header(self.serialized_tx, self.tx, bool(self.segwit)) writers.write_varint(self.serialized_tx, self.tx.inputs_count) for i in range(self.tx.inputs_count): progress.advance() if i in self.segwit: await self.serialize_segwit_input(i) else: await self.sign_nonsegwit_input(i)
async def check_tx_fee(tx: SignTx, root: bip32.HDNode): coin = coins.by_name(tx.coin_name) # h_first is used to make sure the inputs and outputs streamed in Phase 1 # are the same as in Phase 2. it is thus not required to fully hash the # tx, as the SignTx info is streamed only once h_first = HashWriter(sha256) # not a real tx hash bip143 = Bip143() # bip143 transaction hashing multifp = MultisigFingerprint() # control checksum of multisig inputs weight = TxWeightCalculator(tx.inputs_count, tx.outputs_count) total_in = 0 # sum of input amounts segwit_in = 0 # sum of segwit input amounts total_out = 0 # sum of output amounts change_out = 0 # change output amount wallet_path = [] # common prefix of input paths segwit = {} # dict of booleans stating if input is segwit # output structures txo_bin = TxOutputBinType() tx_req = TxRequest() tx_req.details = TxRequestDetailsType() for i in range(tx.inputs_count): progress.advance() # STAGE_REQUEST_1_INPUT txi = await request_tx_input(tx_req, i) wallet_path = input_extract_wallet_path(txi, wallet_path) write_tx_input_check(h_first, txi) weight.add_input(txi) bip143.add_prevouts( txi) # all inputs are included (non-segwit as well) bip143.add_sequence(txi) if txi.multisig: multifp.add(txi.multisig) if txi.script_type in (InputScriptType.SPENDWITNESS, InputScriptType.SPENDP2SHWITNESS): if not coin.segwit: raise SigningError(FailureType.DataError, 'Segwit not enabled on this coin') if not txi.amount: raise SigningError(FailureType.DataError, 'Segwit input without amount') segwit[i] = True segwit_in += txi.amount total_in += txi.amount elif txi.script_type in (InputScriptType.SPENDADDRESS, InputScriptType.SPENDMULTISIG): if coin.force_bip143: if not txi.amount: raise SigningError(FailureType.DataError, 'BIP 143 input without amount') segwit[i] = False segwit_in += txi.amount total_in += txi.amount else: segwit[i] = False total_in += await get_prevtx_output_value( tx_req, txi.prev_hash, txi.prev_index) else: raise SigningError(FailureType.DataError, 'Wrong input script type') for o in range(tx.outputs_count): # STAGE_REQUEST_3_OUTPUT txo = await request_tx_output(tx_req, o) txo_bin.amount = txo.amount txo_bin.script_pubkey = output_derive_script(txo, coin, root) weight.add_output(txo_bin.script_pubkey) if change_out == 0 and is_change(txo, wallet_path, segwit_in, multifp): # output is change and does not need confirmation change_out = txo.amount elif not await confirm_output(txo, coin): raise SigningError(FailureType.ActionCancelled, 'Output cancelled') write_tx_output(h_first, txo_bin) bip143.add_output(txo_bin) total_out += txo_bin.amount fee = total_in - total_out if fee < 0: raise SigningError(FailureType.NotEnoughFunds, 'Not enough funds') # fee > (coin.maxfee per byte * tx size) if fee > (coin.maxfee_kb / 1000) * (weight.get_total() / 4): if not await confirm_feeoverthreshold(fee, coin): raise SigningError(FailureType.ActionCancelled, 'Signing cancelled') if not await confirm_total(total_out - change_out, fee, coin): raise SigningError(FailureType.ActionCancelled, 'Total cancelled') return h_first, bip143, segwit, total_in, wallet_path
async def sign_tx(tx: SignTx, root: bip32.HDNode): tx = sanitize_sign_tx(tx) progress.init(tx.inputs_count, tx.outputs_count) # Phase 1 h_first, bip143, segwit, authorized_in, wallet_path = await check_tx_fee( tx, root) # Phase 2 # - sign inputs # - check that nothing changed coin = coins.by_name(tx.coin_name) tx_ser = TxRequestSerializedType() txo_bin = TxOutputBinType() tx_req = TxRequest() tx_req.details = TxRequestDetailsType() tx_req.serialized = None for i_sign in range(tx.inputs_count): progress.advance() txi_sign = None key_sign = None key_sign_pub = None if segwit[i_sign]: # STAGE_REQUEST_SEGWIT_INPUT txi_sign = await request_tx_input(tx_req, i_sign) is_segwit = (txi_sign.script_type == InputScriptType.SPENDWITNESS or txi_sign.script_type == InputScriptType.SPENDP2SHWITNESS) if not is_segwit: raise SigningError(FailureType.ProcessError, 'Transaction has changed during signing') input_check_wallet_path(txi_sign, wallet_path) key_sign = node_derive(root, txi_sign.address_n) key_sign_pub = key_sign.public_key() txi_sign.script_sig = input_derive_script(coin, txi_sign, key_sign_pub) w_txi = bytearray_with_cap(7 + len(txi_sign.prev_hash) + 4 + len(txi_sign.script_sig) + 4) if i_sign == 0: # serializing first input => prepend headers write_bytes(w_txi, get_tx_header(tx, True)) write_tx_input(w_txi, txi_sign) tx_ser.serialized_tx = w_txi tx_req.serialized = tx_ser elif coin.force_bip143: # STAGE_REQUEST_SEGWIT_INPUT txi_sign = await request_tx_input(tx_req, i_sign) input_check_wallet_path(txi_sign, wallet_path) is_bip143 = (txi_sign.script_type == InputScriptType.SPENDADDRESS or txi_sign.script_type == InputScriptType.SPENDMULTISIG) if not is_bip143 or txi_sign.amount > authorized_in: raise SigningError(FailureType.ProcessError, 'Transaction has changed during signing') authorized_in -= txi_sign.amount key_sign = node_derive(root, txi_sign.address_n) key_sign_pub = key_sign.public_key() bip143_hash = bip143.preimage_hash(tx, txi_sign, ecdsa_hash_pubkey(key_sign_pub), get_hash_type(coin)) # if multisig, check if singing with a key that is included in multisig if txi_sign.multisig: multisig_pubkey_index(txi_sign.multisig, key_sign_pub) signature = ecdsa_sign(key_sign, bip143_hash) tx_ser.signature_index = i_sign tx_ser.signature = signature # serialize input with correct signature txi_sign.script_sig = input_derive_script(coin, txi_sign, key_sign_pub, signature) w_txi_sign = bytearray_with_cap(5 + len(txi_sign.prev_hash) + 4 + len(txi_sign.script_sig) + 4) if i_sign == 0: # serializing first input => prepend headers write_bytes(w_txi_sign, get_tx_header(tx)) write_tx_input(w_txi_sign, txi_sign) tx_ser.serialized_tx = w_txi_sign tx_req.serialized = tx_ser else: # hash of what we are signing with this input h_sign = HashWriter(sha256) # same as h_first, checked before signing the digest h_second = HashWriter(sha256) write_uint32(h_sign, tx.version) write_varint(h_sign, tx.inputs_count) for i in range(tx.inputs_count): # STAGE_REQUEST_4_INPUT txi = await request_tx_input(tx_req, i) input_check_wallet_path(txi, wallet_path) write_tx_input_check(h_second, txi) if i == i_sign: txi_sign = txi key_sign = node_derive(root, txi.address_n) key_sign_pub = key_sign.public_key() # for the signing process the script_sig is equal # to the previous tx's scriptPubKey (P2PKH) or a redeem script (P2SH) if txi_sign.script_type == InputScriptType.SPENDMULTISIG: txi_sign.script_sig = output_script_multisig( multisig_get_pubkeys(txi_sign.multisig), txi_sign.multisig.m) elif txi_sign.script_type == InputScriptType.SPENDADDRESS: txi_sign.script_sig = output_script_p2pkh( ecdsa_hash_pubkey(key_sign_pub)) else: raise SigningError(FailureType.ProcessError, 'Unknown transaction type') else: txi.script_sig = bytes() write_tx_input(h_sign, txi) write_varint(h_sign, tx.outputs_count) for o in range(tx.outputs_count): # STAGE_REQUEST_4_OUTPUT txo = await request_tx_output(tx_req, o) txo_bin.amount = txo.amount txo_bin.script_pubkey = output_derive_script(txo, coin, root) write_tx_output(h_second, txo_bin) write_tx_output(h_sign, txo_bin) write_uint32(h_sign, tx.lock_time) write_uint32(h_sign, get_hash_type(coin)) # check the control digests if get_tx_hash(h_first, False) != get_tx_hash(h_second, False): raise SigningError(FailureType.ProcessError, 'Transaction has changed during signing') # if multisig, check if singing with a key that is included in multisig if txi_sign.multisig: multisig_pubkey_index(txi_sign.multisig, key_sign_pub) # compute the signature from the tx digest signature = ecdsa_sign(key_sign, get_tx_hash(h_sign, True)) tx_ser.signature_index = i_sign tx_ser.signature = signature # serialize input with correct signature txi_sign.script_sig = input_derive_script(coin, txi_sign, key_sign_pub, signature) w_txi_sign = bytearray_with_cap(5 + len(txi_sign.prev_hash) + 4 + len(txi_sign.script_sig) + 4) if i_sign == 0: # serializing first input => prepend headers write_bytes(w_txi_sign, get_tx_header(tx)) write_tx_input(w_txi_sign, txi_sign) tx_ser.serialized_tx = w_txi_sign tx_req.serialized = tx_ser for o in range(tx.outputs_count): progress.advance() # STAGE_REQUEST_5_OUTPUT txo = await request_tx_output(tx_req, o) txo_bin.amount = txo.amount txo_bin.script_pubkey = output_derive_script(txo, coin, root) # serialize output w_txo_bin = bytearray_with_cap(5 + 8 + 5 + len(txo_bin.script_pubkey) + 4) if o == 0: # serializing first output => prepend outputs count write_varint(w_txo_bin, tx.outputs_count) write_tx_output(w_txo_bin, txo_bin) tx_ser.signature_index = None tx_ser.signature = None tx_ser.serialized_tx = w_txo_bin tx_req.serialized = tx_ser any_segwit = True in segwit.values() for i in range(tx.inputs_count): progress.advance() if segwit[i]: # STAGE_REQUEST_SEGWIT_WITNESS txi = await request_tx_input(tx_req, i) input_check_wallet_path(txi, wallet_path) is_segwit = (txi.script_type == InputScriptType.SPENDWITNESS or txi.script_type == InputScriptType.SPENDP2SHWITNESS) if not is_segwit or txi.amount > authorized_in: raise SigningError(FailureType.ProcessError, 'Transaction has changed during signing') authorized_in -= txi.amount key_sign = node_derive(root, txi.address_n) key_sign_pub = key_sign.public_key() bip143_hash = bip143.preimage_hash(tx, txi, ecdsa_hash_pubkey(key_sign_pub), get_hash_type(coin)) signature = ecdsa_sign(key_sign, bip143_hash) if txi.multisig: # find out place of our signature based on the pubkey signature_index = multisig_pubkey_index( txi.multisig, key_sign_pub) witness = witness_p2wsh(txi.multisig, signature, signature_index, get_hash_type(coin)) else: witness = witness_p2wpkh(signature, key_sign_pub, get_hash_type(coin)) tx_ser.serialized_tx = witness tx_ser.signature_index = i tx_ser.signature = signature elif any_segwit: tx_ser.serialized_tx = bytearray( 1) # empty witness for non-segwit inputs tx_ser.signature_index = None tx_ser.signature = None tx_req.serialized = tx_ser write_uint32(tx_ser.serialized_tx, tx.lock_time) await request_tx_finish(tx_req)
async def check_tx_fee(tx: SignTx, root: bip32.HDNode): coin = coins.by_name(tx.coin_name) # h_first is used to make sure the inputs and outputs streamed in Phase 1 # are the same as in Phase 2. it is thus not required to fully hash the # tx, as the SignTx info is streamed only once h_first = HashWriter(sha256()) # not a real tx hash if coin.decred: hash143 = DecredPrefixHasher(tx) # pseudo bip143 prefix hashing tx_ser = TxRequestSerializedType() elif tx.overwintered: if tx.version == 3: hash143 = Zip143() # ZIP-0143 transaction hashing elif tx.version == 4: hash143 = Zip243() # ZIP-0243 transaction hashing else: raise SigningError( FailureType.DataError, "Unsupported version for overwintered transaction", ) else: hash143 = Bip143() # BIP-0143 transaction hashing multifp = MultisigFingerprint() # control checksum of multisig inputs weight = TxWeightCalculator(tx.inputs_count, tx.outputs_count) total_in = 0 # sum of input amounts segwit_in = 0 # sum of segwit input amounts total_out = 0 # sum of output amounts change_out = 0 # change output amount wallet_path = [] # common prefix of input paths segwit = {} # dict of booleans stating if input is segwit # output structures txo_bin = TxOutputBinType() tx_req = TxRequest() tx_req.details = TxRequestDetailsType() for i in range(tx.inputs_count): progress.advance() # STAGE_REQUEST_1_INPUT txi = await request_tx_input(tx_req, i) wallet_path = input_extract_wallet_path(txi, wallet_path) write_tx_input_check(h_first, txi) weight.add_input(txi) hash143.add_prevouts( txi) # all inputs are included (non-segwit as well) hash143.add_sequence(txi) if not address_n_matches_coin(txi.address_n, coin): await confirm_foreign_address(txi.address_n, coin) if txi.multisig: multifp.add(txi.multisig) if txi.script_type in ( InputScriptType.SPENDWITNESS, InputScriptType.SPENDP2SHWITNESS, ): if not coin.segwit: raise SigningError(FailureType.DataError, "Segwit not enabled on this coin") if not txi.amount: raise SigningError(FailureType.DataError, "Segwit input without amount") segwit[i] = True segwit_in += txi.amount total_in += txi.amount elif txi.script_type in ( InputScriptType.SPENDADDRESS, InputScriptType.SPENDMULTISIG, ): if coin.force_bip143 or tx.overwintered: if not txi.amount: raise SigningError(FailureType.DataError, "Expected input with amount") segwit[i] = False segwit_in += txi.amount total_in += txi.amount else: segwit[i] = False total_in += await get_prevtx_output_value( coin, tx_req, txi.prev_hash, txi.prev_index) else: raise SigningError(FailureType.DataError, "Wrong input script type") if coin.decred: w_txi = empty_bytearray(8 if i == 0 else 0 + 9 + len(txi.prev_hash)) if i == 0: # serializing first input => prepend headers write_bytes(w_txi, get_tx_header(coin, tx)) write_tx_input_decred(w_txi, txi) tx_ser.serialized_tx = w_txi tx_req.serialized = tx_ser if coin.decred: hash143.add_output_count(tx) for o in range(tx.outputs_count): # STAGE_REQUEST_3_OUTPUT txo = await request_tx_output(tx_req, o) txo_bin.amount = txo.amount txo_bin.script_pubkey = output_derive_script(txo, coin, root) weight.add_output(txo_bin.script_pubkey) if change_out == 0 and is_change(txo, wallet_path, segwit_in, multifp): # output is change and does not need confirmation change_out = txo.amount elif not await confirm_output(txo, coin): raise SigningError(FailureType.ActionCancelled, "Output cancelled") if coin.decred: if txo.decred_script_version is not None and txo.decred_script_version != 0: raise SigningError( FailureType.ActionCancelled, "Cannot send to output with script version != 0", ) txo_bin.decred_script_version = txo.decred_script_version w_txo_bin = empty_bytearray(4 + 8 + 2 + 4 + len(txo_bin.script_pubkey)) if o == 0: # serializing first output => prepend outputs count write_varint(w_txo_bin, tx.outputs_count) write_tx_output(w_txo_bin, txo_bin) tx_ser.serialized_tx = w_txo_bin tx_req.serialized = tx_ser hash143.set_last_output_bytes(w_txo_bin) write_tx_output(h_first, txo_bin) hash143.add_output(txo_bin) total_out += txo_bin.amount fee = total_in - total_out if fee < 0: raise SigningError(FailureType.NotEnoughFunds, "Not enough funds") # fee > (coin.maxfee per byte * tx size) if fee > (coin.maxfee_kb / 1000) * (weight.get_total() / 4): if not await confirm_feeoverthreshold(fee, coin): raise SigningError(FailureType.ActionCancelled, "Signing cancelled") if not await confirm_total(total_in - change_out, fee, coin): raise SigningError(FailureType.ActionCancelled, "Total cancelled") if coin.decred: hash143.add_locktime_expiry(tx) return h_first, hash143, segwit, total_in, wallet_path
async def sign_tx(tx: SignTx, root: bip32.HDNode): tx = sanitize_sign_tx(tx) progress.init(tx.inputs_count, tx.outputs_count) # Phase 1 h_first, hash143, segwit, authorized_in, wallet_path = await check_tx_fee( tx, root) # Phase 2 # - sign inputs # - check that nothing changed coin = coins.by_name(tx.coin_name) tx_ser = TxRequestSerializedType() txo_bin = TxOutputBinType() tx_req = TxRequest() tx_req.details = TxRequestDetailsType() tx_req.serialized = None if coin.decred: prefix_hash = hash143.prefix_hash() for i_sign in range(tx.inputs_count): progress.advance() txi_sign = None key_sign = None key_sign_pub = None if segwit[i_sign]: # STAGE_REQUEST_SEGWIT_INPUT txi_sign = await request_tx_input(tx_req, i_sign) is_segwit = (txi_sign.script_type == InputScriptType.SPENDWITNESS or txi_sign.script_type == InputScriptType.SPENDP2SHWITNESS) if not is_segwit: raise SigningError(FailureType.ProcessError, "Transaction has changed during signing") input_check_wallet_path(txi_sign, wallet_path) key_sign = node_derive(root, txi_sign.address_n) key_sign_pub = key_sign.public_key() txi_sign.script_sig = input_derive_script(coin, txi_sign, key_sign_pub) w_txi = empty_bytearray(7 + len(txi_sign.prev_hash) + 4 + len(txi_sign.script_sig) + 4) if i_sign == 0: # serializing first input => prepend headers write_bytes(w_txi, get_tx_header(coin, tx, True)) write_tx_input(w_txi, txi_sign) tx_ser.serialized_tx = w_txi tx_req.serialized = tx_ser elif coin.force_bip143 or tx.overwintered: # STAGE_REQUEST_SEGWIT_INPUT txi_sign = await request_tx_input(tx_req, i_sign) input_check_wallet_path(txi_sign, wallet_path) is_bip143 = (txi_sign.script_type == InputScriptType.SPENDADDRESS or txi_sign.script_type == InputScriptType.SPENDMULTISIG) if not is_bip143 or txi_sign.amount > authorized_in: raise SigningError(FailureType.ProcessError, "Transaction has changed during signing") authorized_in -= txi_sign.amount key_sign = node_derive(root, txi_sign.address_n) key_sign_pub = key_sign.public_key() hash143_hash = hash143.preimage_hash( coin, tx, txi_sign, ecdsa_hash_pubkey(key_sign_pub, coin), get_hash_type(coin), ) # if multisig, check if singing with a key that is included in multisig if txi_sign.multisig: multisig_pubkey_index(txi_sign.multisig, key_sign_pub) signature = ecdsa_sign(key_sign, hash143_hash) tx_ser.signature_index = i_sign tx_ser.signature = signature # serialize input with correct signature txi_sign.script_sig = input_derive_script(coin, txi_sign, key_sign_pub, signature) w_txi_sign = empty_bytearray(5 + len(txi_sign.prev_hash) + 4 + len(txi_sign.script_sig) + 4) if i_sign == 0: # serializing first input => prepend headers write_bytes(w_txi_sign, get_tx_header(coin, tx)) write_tx_input(w_txi_sign, txi_sign) tx_ser.serialized_tx = w_txi_sign tx_req.serialized = tx_ser elif coin.decred: txi_sign = await request_tx_input(tx_req, i_sign) input_check_wallet_path(txi_sign, wallet_path) key_sign = node_derive(root, txi_sign.address_n) key_sign_pub = key_sign.public_key() if txi_sign.script_type == InputScriptType.SPENDMULTISIG: prev_pkscript = output_script_multisig( multisig_get_pubkeys(txi_sign.multisig), txi_sign.multisig.m) elif txi_sign.script_type == InputScriptType.SPENDADDRESS: prev_pkscript = output_script_p2pkh( ecdsa_hash_pubkey(key_sign_pub, coin)) else: raise ValueError("Unknown input script type") h_witness = HashWriter(blake256()) write_uint32(h_witness, tx.version | DECRED_SERIALIZE_WITNESS_SIGNING) write_varint(h_witness, tx.inputs_count) for ii in range(tx.inputs_count): if ii == i_sign: write_varint(h_witness, len(prev_pkscript)) write_bytes(h_witness, prev_pkscript) else: write_varint(h_witness, 0) witness_hash = get_tx_hash(h_witness, double=coin.sign_hash_double, reverse=False) h_sign = HashWriter(blake256()) write_uint32(h_sign, DECRED_SIGHASHALL) write_bytes(h_sign, prefix_hash) write_bytes(h_sign, witness_hash) sig_hash = get_tx_hash(h_sign, double=coin.sign_hash_double) signature = ecdsa_sign(key_sign, sig_hash) tx_ser.signature_index = i_sign tx_ser.signature = signature # serialize input with correct signature txi_sign.script_sig = input_derive_script(coin, txi_sign, key_sign_pub, signature) w_txi_sign = empty_bytearray( 8 + 4 + len(hash143.get_last_output_bytes()) if i_sign == 0 else 0 + 16 + 4 + len(txi_sign.script_sig)) if i_sign == 0: write_bytes(w_txi_sign, hash143.get_last_output_bytes()) write_uint32(w_txi_sign, tx.lock_time) write_uint32(w_txi_sign, tx.expiry) write_varint(w_txi_sign, tx.inputs_count) write_tx_input_decred_witness(w_txi_sign, txi_sign) tx_ser.serialized_tx = w_txi_sign tx_req.serialized = tx_ser else: # hash of what we are signing with this input h_sign = HashWriter(sha256()) # same as h_first, checked before signing the digest h_second = HashWriter(sha256()) if tx.overwintered: write_uint32(h_sign, tx.version | OVERWINTERED) # nVersion | fOverwintered write_uint32(h_sign, tx.version_group_id) # nVersionGroupId else: write_uint32(h_sign, tx.version) # nVersion write_varint(h_sign, tx.inputs_count) for i in range(tx.inputs_count): # STAGE_REQUEST_4_INPUT txi = await request_tx_input(tx_req, i) input_check_wallet_path(txi, wallet_path) write_tx_input_check(h_second, txi) if i == i_sign: txi_sign = txi key_sign = node_derive(root, txi.address_n) key_sign_pub = key_sign.public_key() # for the signing process the script_sig is equal # to the previous tx's scriptPubKey (P2PKH) or a redeem script (P2SH) if txi_sign.script_type == InputScriptType.SPENDMULTISIG: txi_sign.script_sig = output_script_multisig( multisig_get_pubkeys(txi_sign.multisig), txi_sign.multisig.m) elif txi_sign.script_type == InputScriptType.SPENDADDRESS: txi_sign.script_sig = output_script_p2pkh( ecdsa_hash_pubkey(key_sign_pub, coin)) if coin.bip115: txi_sign.script_sig += script_replay_protection_bip115( txi_sign.prev_block_hash_bip115, txi_sign.prev_block_height_bip115, ) else: raise SigningError(FailureType.ProcessError, "Unknown transaction type") else: txi.script_sig = bytes() write_tx_input(h_sign, txi) write_varint(h_sign, tx.outputs_count) for o in range(tx.outputs_count): # STAGE_REQUEST_4_OUTPUT txo = await request_tx_output(tx_req, o) txo_bin.amount = txo.amount txo_bin.script_pubkey = output_derive_script(txo, coin, root) write_tx_output(h_second, txo_bin) write_tx_output(h_sign, txo_bin) write_uint32(h_sign, tx.lock_time) if tx.overwintered: write_uint32(h_sign, tx.expiry) # expiryHeight write_varint(h_sign, 0) # nJoinSplit write_uint32(h_sign, get_hash_type(coin)) # check the control digests if get_tx_hash(h_first, False) != get_tx_hash(h_second): raise SigningError(FailureType.ProcessError, "Transaction has changed during signing") # if multisig, check if singing with a key that is included in multisig if txi_sign.multisig: multisig_pubkey_index(txi_sign.multisig, key_sign_pub) # compute the signature from the tx digest signature = ecdsa_sign( key_sign, get_tx_hash(h_sign, double=coin.sign_hash_double)) tx_ser.signature_index = i_sign tx_ser.signature = signature # serialize input with correct signature txi_sign.script_sig = input_derive_script(coin, txi_sign, key_sign_pub, signature) w_txi_sign = empty_bytearray(5 + len(txi_sign.prev_hash) + 4 + len(txi_sign.script_sig) + 4) if i_sign == 0: # serializing first input => prepend headers write_bytes(w_txi_sign, get_tx_header(coin, tx)) write_tx_input(w_txi_sign, txi_sign) tx_ser.serialized_tx = w_txi_sign tx_req.serialized = tx_ser if coin.decred: return await request_tx_finish(tx_req) for o in range(tx.outputs_count): progress.advance() # STAGE_REQUEST_5_OUTPUT txo = await request_tx_output(tx_req, o) txo_bin.amount = txo.amount txo_bin.script_pubkey = output_derive_script(txo, coin, root) # serialize output w_txo_bin = empty_bytearray(5 + 8 + 5 + len(txo_bin.script_pubkey) + 4) if o == 0: # serializing first output => prepend outputs count write_varint(w_txo_bin, tx.outputs_count) write_tx_output(w_txo_bin, txo_bin) tx_ser.signature_index = None tx_ser.signature = None tx_ser.serialized_tx = w_txo_bin tx_req.serialized = tx_ser any_segwit = True in segwit.values() for i in range(tx.inputs_count): progress.advance() if segwit[i]: # STAGE_REQUEST_SEGWIT_WITNESS txi = await request_tx_input(tx_req, i) input_check_wallet_path(txi, wallet_path) is_segwit = (txi.script_type == InputScriptType.SPENDWITNESS or txi.script_type == InputScriptType.SPENDP2SHWITNESS) if not is_segwit or txi.amount > authorized_in: raise SigningError(FailureType.ProcessError, "Transaction has changed during signing") authorized_in -= txi.amount key_sign = node_derive(root, txi.address_n) key_sign_pub = key_sign.public_key() hash143_hash = hash143.preimage_hash( coin, tx, txi, ecdsa_hash_pubkey(key_sign_pub, coin), get_hash_type(coin), ) signature = ecdsa_sign(key_sign, hash143_hash) if txi.multisig: # find out place of our signature based on the pubkey signature_index = multisig_pubkey_index( txi.multisig, key_sign_pub) witness = witness_p2wsh(txi.multisig, signature, signature_index, get_hash_type(coin)) else: witness = witness_p2wpkh(signature, key_sign_pub, get_hash_type(coin)) tx_ser.serialized_tx = witness tx_ser.signature_index = i tx_ser.signature = signature elif any_segwit: tx_ser.serialized_tx = bytearray( 1) # empty witness for non-segwit inputs tx_ser.signature_index = None tx_ser.signature = None tx_req.serialized = tx_ser write_uint32(tx_ser.serialized_tx, tx.lock_time) if tx.overwintered: if tx.version == 3: write_uint32(tx_ser.serialized_tx, tx.expiry) # expiryHeight write_varint(tx_ser.serialized_tx, 0) # nJoinSplit elif tx.version == 4: write_uint32(tx_ser.serialized_tx, tx.expiry) # expiryHeight write_uint64(tx_ser.serialized_tx, 0) # valueBalance write_varint(tx_ser.serialized_tx, 0) # nShieldedSpend write_varint(tx_ser.serialized_tx, 0) # nShieldedOutput write_varint(tx_ser.serialized_tx, 0) # nJoinSplit else: raise SigningError( FailureType.DataError, "Unsupported version for overwintered transaction", ) await request_tx_finish(tx_req)
async def step5_serialize_outputs(self) -> None: writers.write_varint(self.serialized_tx, self.tx.outputs_count) for i in range(self.tx.outputs_count): progress.advance() await self.serialize_output(i)
async def check_tx_fee(tx: SignTx, keychain: seed.Keychain, coin: coininfo.CoinInfo): # h_first is used to make sure the inputs and outputs streamed in Phase 1 # are the same as in Phase 2. it is thus not required to fully hash the # tx, as the SignTx info is streamed only once h_first = utils.HashWriter(sha256()) # not a real tx hash if not utils.BITCOIN_ONLY and coin.decred: hash143 = decred.DecredPrefixHasher(tx) # pseudo BIP-0143 prefix hashing tx_ser = TxRequestSerializedType() elif not utils.BITCOIN_ONLY and coin.overwintered: if tx.version == 3: branch_id = tx.branch_id or 0x5BA81B19 # Overwinter hash143 = zcash.Zip143(branch_id) # ZIP-0143 transaction hashing elif tx.version == 4: branch_id = tx.branch_id or 0x76B809BB # Sapling hash143 = zcash.Zip243(branch_id) # ZIP-0243 transaction hashing else: raise SigningError( FailureType.DataError, "Unsupported version for overwintered transaction", ) else: hash143 = segwit_bip143.Bip143() # BIP-0143 transaction hashing multisig_fp = multisig.MultisigFingerprint() # control checksum of multisig inputs weight = tx_weight.TxWeightCalculator(tx.inputs_count, tx.outputs_count) total_in = 0 # sum of input amounts bip143_in = 0 # sum of segwit input amounts total_out = 0 # sum of output amounts change_out = 0 # change output amount wallet_path = [] # common prefix of input paths segwit = {} # dict of booleans stating if input is segwit # output structures txo_bin = TxOutputBinType() tx_req = TxRequest() tx_req.details = TxRequestDetailsType() for i in range(tx.inputs_count): progress.advance() # STAGE_REQUEST_1_INPUT txi = await helpers.request_tx_input(tx_req, i, coin) wallet_path = input_extract_wallet_path(txi, wallet_path) writers.write_tx_input_check(h_first, txi) weight.add_input(txi) hash143.add_prevouts(txi) # all inputs are included (non-segwit as well) hash143.add_sequence(txi) if not addresses.validate_full_path(txi.address_n, coin, txi.script_type): await helpers.confirm_foreign_address(txi.address_n) if txi.multisig: multisig_fp.add(txi.multisig) else: multisig_fp.mismatch = True if txi.script_type in ( InputScriptType.SPENDWITNESS, InputScriptType.SPENDP2SHWITNESS, ): if not coin.segwit: raise SigningError( FailureType.DataError, "Segwit not enabled on this coin" ) if not txi.amount: raise SigningError(FailureType.DataError, "Segwit input without amount") segwit[i] = True bip143_in += txi.amount total_in += txi.amount elif txi.script_type in ( InputScriptType.SPENDADDRESS, InputScriptType.SPENDMULTISIG, ): if not utils.BITCOIN_ONLY and (coin.force_bip143 or coin.overwintered): if not txi.amount: raise SigningError( FailureType.DataError, "Expected input with amount" ) segwit[i] = False bip143_in += txi.amount total_in += txi.amount else: segwit[i] = False total_in += await get_prevtx_output_value( coin, tx_req, txi.prev_hash, txi.prev_index ) else: raise SigningError(FailureType.DataError, "Wrong input script type") if not utils.BITCOIN_ONLY and coin.decred: w_txi = writers.empty_bytearray(8 if i == 0 else 0 + 9 + len(txi.prev_hash)) if i == 0: # serializing first input => prepend headers # decred doesn't support segwit write_tx_header(w_txi, coin, tx, False) writers.write_tx_input_decred(w_txi, txi) tx_ser.serialized_tx = w_txi tx_req.serialized = tx_ser if not utils.BITCOIN_ONLY and coin.decred: hash143.add_output_count(tx) for o in range(tx.outputs_count): # STAGE_REQUEST_3_OUTPUT txo = await helpers.request_tx_output(tx_req, o, coin) txo_bin.amount = txo.amount txo_bin.script_pubkey = output_derive_script(txo, coin, keychain) weight.add_output(txo_bin.script_pubkey) if change_out == 0 and output_is_change(txo, wallet_path, multisig_fp): # output is change and does not need confirmation change_out = txo.amount elif not await helpers.confirm_output(txo, coin): raise SigningError(FailureType.ActionCancelled, "Output cancelled") if not utils.BITCOIN_ONLY and coin.decred: if txo.decred_script_version is not None and txo.decred_script_version != 0: raise SigningError( FailureType.ActionCancelled, "Cannot send to output with script version != 0", ) txo_bin.decred_script_version = txo.decred_script_version w_txo_bin = writers.empty_bytearray( 4 + 8 + 2 + 4 + len(txo_bin.script_pubkey) ) if o == 0: # serializing first output => prepend outputs count writers.write_varint(w_txo_bin, tx.outputs_count) writers.write_tx_output(w_txo_bin, txo_bin) tx_ser.serialized_tx = w_txo_bin tx_req.serialized = tx_ser hash143.set_last_output_bytes(w_txo_bin) writers.write_tx_output(h_first, txo_bin) hash143.add_output(txo_bin) total_out += txo_bin.amount fee = total_in - total_out if not utils.BITCOIN_ONLY and coin.negative_fee: pass # bypass check for negative fee coins, required for reward TX else: if fee < 0: raise SigningError(FailureType.NotEnoughFunds, "Not enough funds") # fee > (coin.maxfee per byte * tx size) if fee > (coin.maxfee_kb / 1000) * (weight.get_total() / 4): if not await helpers.confirm_feeoverthreshold(fee, coin): raise SigningError(FailureType.ActionCancelled, "Signing cancelled") if tx.lock_time > 0: if not await helpers.confirm_nondefault_locktime(tx.lock_time): raise SigningError(FailureType.ActionCancelled, "Locktime cancelled") if not await helpers.confirm_total(total_in - change_out, fee, coin): raise SigningError(FailureType.ActionCancelled, "Total cancelled") if not utils.BITCOIN_ONLY and coin.decred: hash143.add_locktime_expiry(tx) return h_first, hash143, segwit, bip143_in, wallet_path, multisig_fp
async def sign_tx(tx: SignTx, keychain: seed.Keychain): coin_name = tx.coin_name if tx.coin_name is not None else "Bitcoin" coin = coins.by_name(coin_name) tx = helpers.sanitize_sign_tx(tx, coin) progress.init(tx.inputs_count, tx.outputs_count) # Phase 1 ( h_first, hash143, segwit, authorized_bip143_in, wallet_path, multisig_fp, ) = await check_tx_fee(tx, keychain, coin) # Phase 2 # - sign inputs # - check that nothing changed any_segwit = True in segwit.values() tx_ser = TxRequestSerializedType() txo_bin = TxOutputBinType() tx_req = TxRequest() tx_req.details = TxRequestDetailsType() tx_req.serialized = None if not utils.BITCOIN_ONLY and coin.decred: prefix_hash = hash143.prefix_hash() for i_sign in range(tx.inputs_count): progress.advance() txi_sign = None key_sign = None key_sign_pub = None if segwit[i_sign]: # STAGE_REQUEST_SEGWIT_INPUT txi_sign = await helpers.request_tx_input(tx_req, i_sign, coin) if not input_is_segwit(txi_sign): raise SigningError( FailureType.ProcessError, "Transaction has changed during signing" ) input_check_wallet_path(txi_sign, wallet_path) # NOTE: No need to check the multisig fingerprint, because we won't be signing # the script here. Signatures are produced in STAGE_REQUEST_SEGWIT_WITNESS. key_sign = keychain.derive(txi_sign.address_n, coin.curve_name) key_sign_pub = key_sign.public_key() txi_sign.script_sig = input_derive_script(coin, txi_sign, key_sign_pub) w_txi = writers.empty_bytearray( 7 + len(txi_sign.prev_hash) + 4 + len(txi_sign.script_sig) + 4 ) if i_sign == 0: # serializing first input => prepend headers write_tx_header(w_txi, coin, tx, True) writers.write_tx_input(w_txi, txi_sign) tx_ser.serialized_tx = w_txi tx_ser.signature_index = None tx_ser.signature = None tx_req.serialized = tx_ser elif not utils.BITCOIN_ONLY and (coin.force_bip143 or coin.overwintered): # STAGE_REQUEST_SEGWIT_INPUT txi_sign = await helpers.request_tx_input(tx_req, i_sign, coin) input_check_wallet_path(txi_sign, wallet_path) input_check_multisig_fingerprint(txi_sign, multisig_fp) is_bip143 = ( txi_sign.script_type == InputScriptType.SPENDADDRESS or txi_sign.script_type == InputScriptType.SPENDMULTISIG ) if not is_bip143 or txi_sign.amount > authorized_bip143_in: raise SigningError( FailureType.ProcessError, "Transaction has changed during signing" ) authorized_bip143_in -= txi_sign.amount key_sign = keychain.derive(txi_sign.address_n, coin.curve_name) key_sign_pub = key_sign.public_key() hash143_hash = hash143.preimage_hash( coin, tx, txi_sign, addresses.ecdsa_hash_pubkey(key_sign_pub, coin), get_hash_type(coin), ) # if multisig, check if signing with a key that is included in multisig if txi_sign.multisig: multisig.multisig_pubkey_index(txi_sign.multisig, key_sign_pub) signature = ecdsa_sign(key_sign, hash143_hash) tx_ser.signature_index = i_sign tx_ser.signature = signature # serialize input with correct signature gc.collect() txi_sign.script_sig = input_derive_script( coin, txi_sign, key_sign_pub, signature ) w_txi_sign = writers.empty_bytearray( 5 + len(txi_sign.prev_hash) + 4 + len(txi_sign.script_sig) + 4 ) if i_sign == 0: # serializing first input => prepend headers write_tx_header(w_txi_sign, coin, tx, any_segwit) writers.write_tx_input(w_txi_sign, txi_sign) tx_ser.serialized_tx = w_txi_sign tx_req.serialized = tx_ser elif not utils.BITCOIN_ONLY and coin.decred: txi_sign = await helpers.request_tx_input(tx_req, i_sign, coin) input_check_wallet_path(txi_sign, wallet_path) input_check_multisig_fingerprint(txi_sign, multisig_fp) key_sign = keychain.derive(txi_sign.address_n, coin.curve_name) key_sign_pub = key_sign.public_key() if txi_sign.script_type == InputScriptType.SPENDMULTISIG: prev_pkscript = scripts.output_script_multisig( multisig.multisig_get_pubkeys(txi_sign.multisig), txi_sign.multisig.m, ) elif txi_sign.script_type == InputScriptType.SPENDADDRESS: prev_pkscript = scripts.output_script_p2pkh( addresses.ecdsa_hash_pubkey(key_sign_pub, coin) ) else: raise SigningError("Unsupported input script type") h_witness = utils.HashWriter(blake256()) writers.write_uint32( h_witness, tx.version | decred.DECRED_SERIALIZE_WITNESS_SIGNING ) writers.write_varint(h_witness, tx.inputs_count) for ii in range(tx.inputs_count): if ii == i_sign: writers.write_bytes_prefixed(h_witness, prev_pkscript) else: writers.write_varint(h_witness, 0) witness_hash = writers.get_tx_hash( h_witness, double=coin.sign_hash_double, reverse=False ) h_sign = utils.HashWriter(blake256()) writers.write_uint32(h_sign, decred.DECRED_SIGHASHALL) writers.write_bytes_fixed(h_sign, prefix_hash, writers.TX_HASH_SIZE) writers.write_bytes_fixed(h_sign, witness_hash, writers.TX_HASH_SIZE) sig_hash = writers.get_tx_hash(h_sign, double=coin.sign_hash_double) signature = ecdsa_sign(key_sign, sig_hash) tx_ser.signature_index = i_sign tx_ser.signature = signature # serialize input with correct signature gc.collect() txi_sign.script_sig = input_derive_script( coin, txi_sign, key_sign_pub, signature ) w_txi_sign = writers.empty_bytearray( 8 + 4 + len(hash143.get_last_output_bytes()) if i_sign == 0 else 0 + 16 + 4 + len(txi_sign.script_sig) ) if i_sign == 0: writers.write_bytes_unchecked( w_txi_sign, hash143.get_last_output_bytes() ) writers.write_uint32(w_txi_sign, tx.lock_time) writers.write_uint32(w_txi_sign, tx.expiry) writers.write_varint(w_txi_sign, tx.inputs_count) writers.write_tx_input_decred_witness(w_txi_sign, txi_sign) tx_ser.serialized_tx = w_txi_sign tx_req.serialized = tx_ser else: # hash of what we are signing with this input h_sign = utils.HashWriter(sha256()) # same as h_first, checked before signing the digest h_second = utils.HashWriter(sha256()) writers.write_uint32(h_sign, tx.version) # nVersion if not utils.BITCOIN_ONLY and coin.timestamp: writers.write_uint32(h_sign, tx.timestamp) writers.write_varint(h_sign, tx.inputs_count) for i in range(tx.inputs_count): # STAGE_REQUEST_4_INPUT txi = await helpers.request_tx_input(tx_req, i, coin) input_check_wallet_path(txi, wallet_path) writers.write_tx_input_check(h_second, txi) if i == i_sign: txi_sign = txi input_check_multisig_fingerprint(txi_sign, multisig_fp) key_sign = keychain.derive(txi.address_n, coin.curve_name) key_sign_pub = key_sign.public_key() # for the signing process the script_sig is equal # to the previous tx's scriptPubKey (P2PKH) or a redeem script (P2SH) if txi_sign.script_type == InputScriptType.SPENDMULTISIG: txi_sign.script_sig = scripts.output_script_multisig( multisig.multisig_get_pubkeys(txi_sign.multisig), txi_sign.multisig.m, ) elif txi_sign.script_type == InputScriptType.SPENDADDRESS: txi_sign.script_sig = scripts.output_script_p2pkh( addresses.ecdsa_hash_pubkey(key_sign_pub, coin) ) else: raise SigningError( FailureType.ProcessError, "Unknown transaction type" ) else: txi.script_sig = bytes() writers.write_tx_input(h_sign, txi) writers.write_varint(h_sign, tx.outputs_count) for o in range(tx.outputs_count): # STAGE_REQUEST_4_OUTPUT txo = await helpers.request_tx_output(tx_req, o, coin) txo_bin.amount = txo.amount txo_bin.script_pubkey = output_derive_script(txo, coin, keychain) writers.write_tx_output(h_second, txo_bin) writers.write_tx_output(h_sign, txo_bin) writers.write_uint32(h_sign, tx.lock_time) writers.write_uint32(h_sign, get_hash_type(coin)) # check the control digests if writers.get_tx_hash(h_first, False) != writers.get_tx_hash(h_second): raise SigningError( FailureType.ProcessError, "Transaction has changed during signing" ) # if multisig, check if signing with a key that is included in multisig if txi_sign.multisig: multisig.multisig_pubkey_index(txi_sign.multisig, key_sign_pub) # compute the signature from the tx digest signature = ecdsa_sign( key_sign, writers.get_tx_hash(h_sign, double=coin.sign_hash_double) ) tx_ser.signature_index = i_sign tx_ser.signature = signature # serialize input with correct signature gc.collect() txi_sign.script_sig = input_derive_script( coin, txi_sign, key_sign_pub, signature ) w_txi_sign = writers.empty_bytearray( 5 + len(txi_sign.prev_hash) + 4 + len(txi_sign.script_sig) + 4 ) if i_sign == 0: # serializing first input => prepend headers write_tx_header(w_txi_sign, coin, tx, any_segwit) writers.write_tx_input(w_txi_sign, txi_sign) tx_ser.serialized_tx = w_txi_sign tx_req.serialized = tx_ser if not utils.BITCOIN_ONLY and coin.decred: return await helpers.request_tx_finish(tx_req) for o in range(tx.outputs_count): progress.advance() # STAGE_REQUEST_5_OUTPUT txo = await helpers.request_tx_output(tx_req, o, coin) txo_bin.amount = txo.amount txo_bin.script_pubkey = output_derive_script(txo, coin, keychain) # serialize output w_txo_bin = writers.empty_bytearray(5 + 8 + 5 + len(txo_bin.script_pubkey) + 4) if o == 0: # serializing first output => prepend outputs count writers.write_varint(w_txo_bin, tx.outputs_count) writers.write_tx_output(w_txo_bin, txo_bin) tx_ser.signature_index = None tx_ser.signature = None tx_ser.serialized_tx = w_txo_bin tx_req.serialized = tx_ser for i in range(tx.inputs_count): progress.advance() if segwit[i]: # STAGE_REQUEST_SEGWIT_WITNESS txi = await helpers.request_tx_input(tx_req, i, coin) input_check_wallet_path(txi, wallet_path) input_check_multisig_fingerprint(txi, multisig_fp) if not input_is_segwit(txi) or txi.amount > authorized_bip143_in: raise SigningError( FailureType.ProcessError, "Transaction has changed during signing" ) authorized_bip143_in -= txi.amount key_sign = keychain.derive(txi.address_n, coin.curve_name) key_sign_pub = key_sign.public_key() hash143_hash = hash143.preimage_hash( coin, tx, txi, addresses.ecdsa_hash_pubkey(key_sign_pub, coin), get_hash_type(coin), ) signature = ecdsa_sign(key_sign, hash143_hash) if txi.multisig: # find out place of our signature based on the pubkey signature_index = multisig.multisig_pubkey_index( txi.multisig, key_sign_pub ) witness = scripts.witness_p2wsh( txi.multisig, signature, signature_index, get_hash_type(coin) ) else: witness = scripts.witness_p2wpkh( signature, key_sign_pub, get_hash_type(coin) ) tx_ser.serialized_tx = witness tx_ser.signature_index = i tx_ser.signature = signature elif any_segwit: tx_ser.serialized_tx += bytearray(1) # empty witness for non-segwit inputs tx_ser.signature_index = None tx_ser.signature = None tx_req.serialized = tx_ser writers.write_uint32(tx_ser.serialized_tx, tx.lock_time) if not utils.BITCOIN_ONLY and coin.overwintered: if tx.version == 3: writers.write_uint32(tx_ser.serialized_tx, tx.expiry) # expiryHeight writers.write_varint(tx_ser.serialized_tx, 0) # nJoinSplit elif tx.version == 4: writers.write_uint32(tx_ser.serialized_tx, tx.expiry) # expiryHeight writers.write_uint64(tx_ser.serialized_tx, 0) # valueBalance writers.write_varint(tx_ser.serialized_tx, 0) # nShieldedSpend writers.write_varint(tx_ser.serialized_tx, 0) # nShieldedOutput writers.write_varint(tx_ser.serialized_tx, 0) # nJoinSplit else: raise SigningError( FailureType.DataError, "Unsupported version for overwintered transaction", ) await helpers.request_tx_finish(tx_req)