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 confirm_output(self, txo: TxOutputType, script_pubkey: bytes) -> None: if txo.decred_script_version != 0: raise SigningError( FailureType.ActionCancelled, "Cannot send to output with script version != 0", ) await super().confirm_output(txo, script_pubkey) self.write_tx_output(self.serialized_tx, txo, script_pubkey)
async def get_prevtx_output_value(self, prev_hash: bytes, prev_index: int) -> int: amount_out = 0 # output amount # STAGE_REQUEST_2_PREV_META in legacy tx = await helpers.request_tx_meta(self.tx_req, self.coin, prev_hash) if tx.outputs_cnt <= prev_index: raise SigningError( FailureType.ProcessError, "Not enough outputs in previous transaction." ) txh = self.create_hash_writer() # witnesses are not included in txid hash self.write_tx_header(txh, tx, witness_marker=False) writers.write_varint(txh, tx.inputs_cnt) for i in range(tx.inputs_cnt): # STAGE_REQUEST_2_PREV_INPUT in legacy txi = await helpers.request_tx_input(self.tx_req, i, self.coin, prev_hash) self.write_tx_input(txh, txi, txi.script_sig) writers.write_varint(txh, tx.outputs_cnt) for i in range(tx.outputs_cnt): # STAGE_REQUEST_2_PREV_OUTPUT in legacy txo_bin = await helpers.request_tx_output( self.tx_req, i, self.coin, prev_hash ) self.write_tx_output(txh, txo_bin, txo_bin.script_pubkey) if i == prev_index: amount_out = txo_bin.amount self.check_prevtx_output(txo_bin) await self.write_prev_tx_footer(txh, tx, prev_hash) if ( writers.get_tx_hash(txh, double=self.coin.sign_hash_double, reverse=True) != prev_hash ): raise SigningError( FailureType.ProcessError, "Encountered invalid prev_hash" ) return amount_out
def check_input(self, txi: TxInputType) -> None: if self.attribute is self.MISMATCH: return # There was already a mismatch when adding inputs, ignore it now. # All added inputs had a matching attribute, allowing a change-output. # Ensure that this input still has the same attribute. if self.attribute != self.attribute_from_tx(txi): raise SigningError(FailureType.ProcessError, "Transaction has changed during signing")
async def step3_confirm_tx(self) -> None: fee = self.total_in - self.total_out if fee < 0: self.on_negative_fee() # fee > (coin.maxfee per byte * tx size) if fee > (self.coin.maxfee_kb / 1000) * (self.weight.get_total() / 4): if not await helpers.confirm_feeoverthreshold(fee, self.coin): raise SigningError(FailureType.ActionCancelled, "Signing cancelled") if self.tx.lock_time > 0: if not await helpers.confirm_nondefault_locktime(self.tx.lock_time): raise SigningError(FailureType.ActionCancelled, "Locktime cancelled") if not await helpers.confirm_total( self.total_in - self.change_out, fee, self.coin ): raise SigningError(FailureType.ActionCancelled, "Total cancelled")
async def confirm_output(self, txo: TxOutputType, script_pubkey: bytes) -> None: if self.change_out == 0 and self.output_is_change(txo): # output is change and does not need confirmation self.change_out = txo.amount elif not await helpers.confirm_output(txo, self.coin): raise SigningError(FailureType.ActionCancelled, "Output cancelled") self.write_tx_output(self.h_confirmed, txo, script_pubkey) self.hash143_add_output(txo, script_pubkey) self.total_out += txo.amount
def output_derive_script(self, txo: TxOutputType) -> bytes: if txo.address_n: # change output try: input_script_type = helpers.CHANGE_OUTPUT_TO_INPUT_SCRIPT_TYPES[ txo.script_type] except KeyError: raise SigningError(FailureType.DataError, "Invalid script type") node = self.keychain.derive(txo.address_n) txo.address = addresses.get_address(input_script_type, self.coin, node, txo.multisig) return scripts.output_derive_script(txo, self.coin)
async def serialize_segwit_input(self, i: int) -> None: # STAGE_REQUEST_SEGWIT_INPUT in legacy txi = await helpers.request_tx_input(self.tx_req, i, self.coin) if not input_is_segwit(txi): raise SigningError(FailureType.ProcessError, "Transaction has changed during signing") self.wallet_path.check_input(txi) # 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. node = self.keychain.derive(txi.address_n) key_sign_pub = node.public_key() script_sig = self.input_derive_script(txi, key_sign_pub) self.write_tx_input(self.serialized_tx, txi, script_sig)
def derive_script_code(txi: TxInputType, pubkeyhash: bytes) -> bytearray: if txi.multisig: return output_script_multisig( multisig_get_pubkeys(txi.multisig), txi.multisig.m ) p2pkh = txi.script_type == InputScriptType.SPENDADDRESS if p2pkh: return output_script_p2pkh(pubkeyhash) else: raise SigningError( FailureType.DataError, "Unknown input script type for zip143 script code" )
async def process_input(self, txi: TxInputType) -> None: self.wallet_path.add_input(txi) self.multisig_fingerprint.add_input(txi) writers.write_tx_input_check(self.h_confirmed, txi) self.hash143_add_input(txi) # all inputs are included (non-segwit as well) if not addresses.validate_full_path(txi.address_n, self.coin, txi.script_type): await helpers.confirm_foreign_address(txi.address_n) if input_is_segwit(txi): await self.process_segwit_input(txi) elif input_is_nonsegwit(txi): await self.process_nonsegwit_input(txi) else: raise SigningError(FailureType.DataError, "Wrong input script type")
def __init__(self, tx: SignTx, keychain: Keychain, coin: CoinInfo) -> None: ensure(coin.overwintered) super().__init__(tx, keychain, coin) if self.tx.version == 3: if not self.tx.branch_id: self.tx.branch_id = 0x5BA81B19 # Overwinter elif self.tx.version == 4: if not self.tx.branch_id: self.tx.branch_id = 0x76B809BB # Sapling else: raise SigningError( FailureType.DataError, "Unsupported version for overwintered transaction", )
async def sign_nonsegwit_bip143_input(self, i_sign: int) -> None: txi = await helpers.request_tx_input(self.tx_req, i_sign, self.coin) if not input_is_nonsegwit(txi): raise SigningError(FailureType.ProcessError, "Transaction has changed during signing") public_key, signature = self.sign_bip143_input(txi) # if multisig, do a sanity check to ensure we are signing with a key that is included in the multisig if txi.multisig: multisig.multisig_pubkey_index(txi.multisig, public_key) # serialize input with correct signature gc.collect() script_sig = self.input_derive_script(txi, public_key, signature) self.write_tx_input(self.serialized_tx, txi, script_sig) self.set_serialized_signature(i_sign, signature)
def sign_bip143_input(self, txi: TxInputType) -> Tuple[bytes, bytes]: self.wallet_path.check_input(txi) self.multisig_fingerprint.check_input(txi) if txi.amount > self.bip143_in: raise SigningError(FailureType.ProcessError, "Transaction has changed during signing") self.bip143_in -= txi.amount node = self.keychain.derive(txi.address_n) public_key = node.public_key() hash143_hash = self.hash143_preimage_hash( txi, addresses.ecdsa_hash_pubkey(public_key, self.coin)) signature = ecdsa_sign(node, hash143_hash) return public_key, signature
async def step7_finish(self) -> None: self.write_tx_footer(self.serialized_tx, self.tx) if self.tx.version == 3: write_uint32(self.serialized_tx, self.tx.expiry) # expiryHeight write_varint(self.serialized_tx, 0) # nJoinSplit elif self.tx.version == 4: write_uint32(self.serialized_tx, self.tx.expiry) # expiryHeight write_uint64(self.serialized_tx, 0) # valueBalance write_varint(self.serialized_tx, 0) # nShieldedSpend write_varint(self.serialized_tx, 0) # nShieldedOutput write_varint(self.serialized_tx, 0) # nJoinSplit else: raise SigningError( FailureType.DataError, "Unsupported version for overwintered transaction", ) await helpers.request_tx_finish(self.tx_req)
async def sign_segwit_input(self, i: int) -> None: # STAGE_REQUEST_SEGWIT_WITNESS in legacy txi = await helpers.request_tx_input(self.tx_req, i, self.coin) if not input_is_segwit(txi): raise SigningError(FailureType.ProcessError, "Transaction has changed during signing") public_key, signature = self.sign_bip143_input(txi) self.set_serialized_signature(i, signature) if txi.multisig: # find out place of our signature based on the pubkey signature_index = multisig.multisig_pubkey_index( txi.multisig, public_key) self.serialized_tx.extend( scripts.witness_p2wsh(txi.multisig, signature, signature_index, self.get_hash_type())) else: self.serialized_tx.extend( scripts.witness_p2wpkh(signature, public_key, self.get_hash_type()))
def check_prevtx_output(self, txo_bin: TxOutputBinType) -> None: if txo_bin.decred_script_version != 0: raise SigningError( FailureType.ProcessError, "Cannot use utxo that has script_version != 0", )
async def sign_nonsegwit_input(self, i_sign: int) -> None: # hash of what we are signing with this input h_sign = self.create_hash_writer() # should come out the same as h_confirmed, checked before signing the digest h_check = self.create_hash_writer() self.write_tx_header(h_sign, self.tx, witness_marker=False) writers.write_varint(h_sign, self.tx.inputs_count) for i in range(self.tx.inputs_count): # STAGE_REQUEST_4_INPUT in legacy txi = await helpers.request_tx_input(self.tx_req, i, self.coin) writers.write_tx_input_check(h_check, txi) if i == i_sign: self.wallet_path.check_input(txi) self.multisig_fingerprint.check_input(txi) # NOTE: wallet_path is checked in write_tx_input_check() node = self.keychain.derive(txi.address_n, self.coin.curve_name) key_sign_pub = node.public_key() # if multisig, do a sanity check to ensure we are signing with a key that is included in the multisig if txi.multisig: multisig.multisig_pubkey_index(txi.multisig, key_sign_pub) # For the signing process the previous UTXO's scriptPubKey is included in h_sign. if txi.script_type == InputScriptType.SPENDMULTISIG: script_pubkey = scripts.output_script_multisig( multisig.multisig_get_pubkeys(txi.multisig), txi.multisig.m, ) elif txi.script_type == InputScriptType.SPENDADDRESS: script_pubkey = scripts.output_script_p2pkh( addresses.ecdsa_hash_pubkey(key_sign_pub, self.coin) ) else: raise SigningError( FailureType.ProcessError, "Unknown transaction type" ) txi_sign = txi else: script_pubkey = bytes() self.write_tx_input(h_sign, txi, script_pubkey) writers.write_varint(h_sign, self.tx.outputs_count) for i in range(self.tx.outputs_count): # STAGE_REQUEST_4_OUTPUT in legacy txo = await helpers.request_tx_output(self.tx_req, i, self.coin) script_pubkey = self.output_derive_script(txo) self.write_tx_output(h_check, txo, script_pubkey) self.write_tx_output(h_sign, txo, script_pubkey) writers.write_uint32(h_sign, self.tx.lock_time) writers.write_uint32(h_sign, self.get_hash_type()) # check the control digests if self.h_confirmed.get_digest() != h_check.get_digest(): raise SigningError( FailureType.ProcessError, "Transaction has changed during signing" ) # compute the signature from the tx digest signature = ecdsa_sign( node, writers.get_tx_hash(h_sign, double=self.coin.sign_hash_double) ) # serialize input with correct signature gc.collect() script_sig = self.input_derive_script(txi_sign, key_sign_pub, signature) self.write_tx_input(self.serialized_tx, txi_sign, script_sig) self.set_serialized_signature(i_sign, signature)
def on_negative_fee(self) -> None: raise SigningError(FailureType.NotEnoughFunds, "Not enough funds")
def hash143_preimage_hash(self, txi: TxInputType, pubkeyhash: bytes) -> bytes: h_preimage = HashWriter( blake2b( outlen=32, personal=b"ZcashSigHash" + struct.pack("<I", self.tx.branch_id), ) ) # 1. nVersion | fOverwintered write_uint32(h_preimage, self.tx.version | OVERWINTERED) # 2. nVersionGroupId write_uint32(h_preimage, self.tx.version_group_id) # 3. hashPrevouts write_bytes_fixed(h_preimage, get_tx_hash(self.h_prevouts), TX_HASH_SIZE) # 4. hashSequence write_bytes_fixed(h_preimage, get_tx_hash(self.h_sequence), TX_HASH_SIZE) # 5. hashOutputs write_bytes_fixed(h_preimage, get_tx_hash(self.h_outputs), TX_HASH_SIZE) if self.tx.version == 3: # 6. hashJoinSplits write_bytes_fixed(h_preimage, b"\x00" * TX_HASH_SIZE, TX_HASH_SIZE) # 7. nLockTime write_uint32(h_preimage, self.tx.lock_time) # 8. expiryHeight write_uint32(h_preimage, self.tx.expiry) # 9. nHashType write_uint32(h_preimage, self.get_hash_type()) elif self.tx.version == 4: zero_hash = b"\x00" * TX_HASH_SIZE # 6. hashJoinSplits write_bytes_fixed(h_preimage, zero_hash, TX_HASH_SIZE) # 7. hashShieldedSpends write_bytes_fixed(h_preimage, zero_hash, TX_HASH_SIZE) # 8. hashShieldedOutputs write_bytes_fixed(h_preimage, zero_hash, TX_HASH_SIZE) # 9. nLockTime write_uint32(h_preimage, self.tx.lock_time) # 10. expiryHeight write_uint32(h_preimage, self.tx.expiry) # 11. valueBalance write_uint64(h_preimage, 0) # 12. nHashType write_uint32(h_preimage, self.get_hash_type()) else: raise SigningError( FailureType.DataError, "Unsupported version for overwintered transaction", ) # 10a /13a. outpoint write_bytes_reversed(h_preimage, txi.prev_hash, TX_HASH_SIZE) write_uint32(h_preimage, txi.prev_index) # 10b / 13b. scriptCode script_code = derive_script_code(txi, pubkeyhash) write_bytes_prefixed(h_preimage, script_code) # 10c / 13c. value write_uint64(h_preimage, txi.amount) # 10d / 13d. nSequence write_uint32(h_preimage, txi.sequence) return get_tx_hash(h_preimage)
async def process_bip143_input(self, txi: TxInputType) -> None: if not txi.amount: raise SigningError(FailureType.DataError, "Expected input with amount") self.bip143_in += txi.amount self.total_in += txi.amount
async def process_segwit_input(self, txi: TxInputType) -> None: if not self.coin.segwit: raise SigningError(FailureType.DataError, "Segwit not enabled on this coin") await super().process_segwit_input(txi)