def _validity_checking(self): '''A variety of tests to ensure this PSBT is valid''' # Check to make sure unsigned transaction is present if PSBT_GLOBAL_UNSIGNED_TX not in self.maps['global']: raise ValueError('Invalid PSBT, missing unsigned transaction') else: # Parse global unsigned tx for future checks tx_obj = Tx.parse(BytesIO(self.maps['global'][PSBT_GLOBAL_UNSIGNED_TX])) # If a scriptSig or scriptWitness is present, this is an signed transaction and is invalid for i in tx_obj.tx_ins: if len(i.script_sig.elements) > 0 or len(i.witness_program) > 1: raise ValueError('Invalid PSBT, transaction in global map has scriptSig or scriptWitness present, not unsigned') # Get number of inputs in unsigned tx and psbt maps global_ins_cnt = len(tx_obj.tx_ins) psbt_ins_cnt = len(self.maps['inputs']) # If unsigned tx has no inputs, invalid if global_ins_cnt == 0: raise ValueError('Invalid PSBT, unsigned transaction missing inputs') # If the psbt has no inputs, invalid elif psbt_ins_cnt == 0: raise ValueError('Invalid PSBT, no inputs') # If the counts do not match, invalid elif global_ins_cnt != psbt_ins_cnt: raise ValueError('Invalid PSBT, number of inputs in unsigned transaction and PSBT do not match') # Repeat for outputs global_outs_cnt = len(tx_obj.tx_outs) psbt_outs_cnt = len(self.maps['outputs']) if global_outs_cnt == 0: raise ValueError('Invalid PSBT, unsigned transaction missing outputs') elif psbt_outs_cnt == 0: raise ValueError('Invalid PSBT, no outputs') elif global_outs_cnt != psbt_outs_cnt: raise ValueError('Invalid PSBT, number of outputs in unsigned transaction and PSBT do not match')
def __init__(self, serialized_psbt): # Specify current role as string for default file name in make_file() self.role = 'Transaction_Extractor' self.psbt=psbt.parse(BytesIO(serialized_psbt)) # Take the finalized scriptSig and scriptWitness data and complete the unsigned tx self.tx_obj = Tx.parse(BytesIO(self.psbt.maps['global'][PSBT_GLOBAL_UNSIGNED_TX])) # Iterate through psbt input key-value fields and input their finalized data into # the transaction. Note this assumes PSBT inputs are ordered the same as they are # in the unsigned TX # TODO: Reconsider that assumption for i in range(len(self.psbt.maps['inputs'])): curr_input = self.psbt.maps['inputs'][i] if self._is_witness_input(curr_input): try: # Insert final scriptWitness as witness program for this input self.tx_obj.tx_ins[i].witness_program = curr_input[PSBT_IN_FINAL_SCRIPTWITNESS] except KeyError: # If this is witness input, final scriptWitness should be present raise ValueError('PSBT input is missing finalized scriptWitness') # If a final scriptSig is present, must be P2SH-wrapped segwit so scriptSig is required if PSBT_IN_FINAL_SCRIPTSIG in curr_input: self.tx_obj.tx_ins[i].script_sig = Script.parse(curr_input[PSBT_IN_FINAL_SCRIPTSIG]) # Else, this is a non-witness input else: try: # Insert final scriptSig into input self.tx_obj.tx_ins[i].script_sig = Script.parse(curr_input[PSBT_IN_FINAL_SCRIPTSIG]) except KeyError: # If not a witness input, final scriptSig should be present raise ValueError('PSBT input is missing finalized scriptSig')
def add_witness_utxo(self, input_index, utxo, utxo_index): '''Add a non-witness UTXO to it's corresponding input input_index - (int) index of the input being updated utxo - raw bytes of utxo being added ''' tx_obj = Tx.parse(BytesIO(utxo)) value = tx_obj.tx_outs[utxo_index].serialize() self.psbt.maps['inputs'][input_index][PSBT_IN_WITNESS_UTXO] = value
def __init__(self, inputs, outputs, tx_version=2, input_sequence=0xffffffff, locktime=0): '''Inputs should be a list of tuples in the form of (prev tx, prev index) Outputs should be a list of tuples in the form of (amount, scriptPubKey)''' # Specify current role as string for default file name in make_file() self.role = 'Creator' self.tx_inputs = [] # outputs should be a list of tuples in the form of (amount, scriptPubKey), amount in satoshi self.tx_outputs = [] # Initialize list of TxIn objects (Inputs) for i in inputs: self.tx_inputs.append(TxIn(prev_tx=i[0], prev_index=i[1], script_sig=b'', sequence=input_sequence)) # Initialize list of TxOut objects (Outputs) for i in outputs: self.tx_outputs.append(TxOut(amount=i[0], script_pubkey=i[1])) self.tx_obj = Tx(version=tx_version, tx_ins=self.tx_inputs, tx_outs=self.tx_outputs, locktime=locktime) # Get a serialized version of the unsigned tx for the psbt serialized_tx = self.tx_obj.serialize() # Construct a serialized psbt manually new_psbt_serialized = MAGIC_BYTES + HEAD_SEPARATOR + psbt.serialize_map(key=PSBT_GLOBAL_UNSIGNED_TX, \ value=serialized_tx) + DATA_SEPARATOR + (DATA_SEPARATOR*len(self.tx_inputs)) + \ (DATA_SEPARATOR*len(self.tx_inputs)) # Create the psbt object using the serialized psbt self.psbt = psbt.parse(BytesIO(new_psbt_serialized))
class Creator(PSBT_Role): '''The Creator creates a new psbt. It must create an unsigned transaction and place it in the psbt. The Creator must create empty input fields.''' def __init__(self, inputs, outputs, tx_version=2, input_sequence=0xffffffff, locktime=0): '''Inputs should be a list of tuples in the form of (prev tx, prev index) Outputs should be a list of tuples in the form of (amount, scriptPubKey)''' # Specify current role as string for default file name in make_file() self.role = 'Creator' self.tx_inputs = [] # outputs should be a list of tuples in the form of (amount, scriptPubKey), amount in satoshi self.tx_outputs = [] # Initialize list of TxIn objects (Inputs) for i in inputs: self.tx_inputs.append(TxIn(prev_tx=i[0], prev_index=i[1], script_sig=b'', sequence=input_sequence)) # Initialize list of TxOut objects (Outputs) for i in outputs: self.tx_outputs.append(TxOut(amount=i[0], script_pubkey=i[1])) self.tx_obj = Tx(version=tx_version, tx_ins=self.tx_inputs, tx_outs=self.tx_outputs, locktime=locktime) # Get a serialized version of the unsigned tx for the psbt serialized_tx = self.tx_obj.serialize() # Construct a serialized psbt manually new_psbt_serialized = MAGIC_BYTES + HEAD_SEPARATOR + psbt.serialize_map(key=PSBT_GLOBAL_UNSIGNED_TX, \ value=serialized_tx) + DATA_SEPARATOR + (DATA_SEPARATOR*len(self.tx_inputs)) + \ (DATA_SEPARATOR*len(self.tx_inputs)) # Create the psbt object using the serialized psbt self.psbt = psbt.parse(BytesIO(new_psbt_serialized)) def get_utxo(self, input_index): raise RuntimeError('Function out of scope for this role') def _get_input_index(self, pubkey): raise RuntimeError('Function out of scope for this role') def _is_witness_input(self, an_input): raise RuntimeError('Function out of scope for this role') def get_output_redeem_script(self, output_index): raise RuntimeError('Function out of scope for this role') def get_output_witness_script(self, output_index): raise RuntimeError('Function out of scope for this role')
def add_partial_signature(self, new_sig, compressed_sec, input_index=None): '''Adds signature to input of PSBT. Signature and public key should be of type bytes If the public key has been added to an input in the PSBT, the input index will be found ''' # TODO: Add more ways to find an input that matches the provided public key # If an input index is not specified in arguments, find it based on matching public key if input_index == None: input_index = self._get_input_index(compressed_sec) # Note that the below assumes that the sighash type is only the last byte of sig. # This may be problematic # Note: Assumes inputs in psbt and indexed the same as in unsigned tx # TODO: Check on this this_sighash = little_endian_to_int(new_sig[-1:]) if input_index is not None: # Check to make sure signature's sighash type correctly matches the type specified # for this input if not self.check_sighash(input_index=input_index, sighash=this_sighash): raise ValueError('Sighash type {} on this signature does not match specified \ sighash type {} for this input'.format(little_endian_to_int(this_sighash), self.get_sighash_type(input_index))) curr_input = self.psbt.maps['inputs'][input_index] # Verify that if UTXO for witness or non-witness is present, it matches TXID of global unsigned tx if PSBT_IN_NON_WITNESS_UTXO in curr_input: global_txid = Tx.parse(BytesIO(self.psbt.maps['global'][PSBT_GLOBAL_UNSIGNED_TX])).tx_ins[input_index].prev_tx utxo_txid = double_sha256(curr_input[PSBT_IN_NON_WITNESS_UTXO])[::-1] # Verify that txids match if utxo_txid != global_txid: raise RuntimeError('UTXO of this input does not match with that in global unsigned tx') # If witness UTXO, verify that hashes match there elif PSBT_IN_WITNESS_UTXO in curr_input: # TODO: Do more testing for native segwit utxos # Get the hash of witness program in scriptPubKey of the witness UTXO scriptPubKey = Script.parse(curr_input[PSBT_IN_WITNESS_UTXO][9:]) # Determine script type of scriptPubKey if scriptPubKey.type() == 'p2wpkh': # If scriptPubKey is p2wpkh, keyhash is last element keyhash = scriptPubKey.elements[-1] # Check to make sure hash160 of compressed pubkey of signature matches that in UTXO scriptPubKey if hash160(compressed_sec) != keyhash: raise RuntimeError('Hash of compressed pubkey in partial signature does not match \ that of hash in witness UTXOs scriptPubKey') elif scriptPubKey.type() == 'p2wsh': # If scriptPubKey is p2wsh, scripthash is last element scripthash = scriptPubKey.elements[-1] # Check to make sure single SHA256 of witnessScript (if provided) matches that in UTXO scriptPubKey if PSBT_IN_WITNESS_SCRIPT in curr_input: if sha256(curr_input[PSBT_IN_WITNESS_SCRIPT]).digest != scripthash: raise RuntimeError('Hash of witnessScript does not match that of hash in witness UTXOs \ scriptPubKey') # Otherwise check if P2SH (including P2SH wrapped segwit) elif scriptPubKey.type() == 'p2sh': # If scriptPubKey is p2sh, scripthash is 2nd to last element scripthash = scriptPubKey.elements[-2] # If redeemScript is present for this input, its hash160 should match scripthash in scriptPubKey if PSBT_IN_REDEEM_SCRIPT in curr_input: if hash160(curr_input[PSBT_IN_REDEEM_SCRIPT]) != scripthash: raise RuntimeError('Hash of redeemScript does not match that of hash in witness UTXOs \ scriptPubKey') # If witness script is also present, verify the hash of this input's witnessScript matches the hash # inside the redeemScript if PSBT_IN_WITNESS_SCRIPT in curr_input: # Parse redeemScript to get hash of witnessScript inside it redeemScript = Script.parse(curr_input[PSBT_IN_REDEEM_SCRIPT]) # The last item in a P2SH-segwit redeemScript is the hash of the witness program redeem_wit_hash = redeemScript.elements[-1] if redeemScript.type() == 'p2wsh': if sha256(curr_input[PSBT_IN_WITNESS_SCRIPT]).digest() != redeem_wit_hash: raise RuntimeError('Hash of witnessScript does not match that of hash in redeemScript') # If this point has been reached without any errors, add partial signature self.psbt.maps['inputs'][input_index][PSBT_IN_PARTIAL_SIG+compressed_sec] = new_sig # If input_index is still None, partial signature cannot be added else: raise RuntimeError('If the public key for this signature has not been added to the PSBT \ and an input_index has not been provided then partial signature cannot be added') return
def parse(cls, s): '''Takes byte stream of a serialized psbt and returns a psbt object.''' # Check that serialization begins with the magic bytes if s.read(4) != MAGIC_BYTES: raise RuntimeError('Missing magic bytes') # Check that that magic bytes are followed by the separator if s.read(1) != HEAD_SEPARATOR : raise RuntimeError('Missing head separator') # Begin parsing global types, which is required to start with an unsigned transaction new_map = { 'global' : {}, 'inputs' : [], 'outputs' : [] } expect_globals = True num_inputs = 0 num_outputs = 0 while expect_globals or num_inputs > 0 or num_outputs > 0: try: new_key, new_value = psbt.parse_key(s) except IndexError: raise RuntimeError('Unexpected serialization encountered, possible missing input/output maps') if expect_globals: # If a separator has been reached, the new_key will be None # So it's time to continue on if new_key == None: expect_globals = False continue # Add new key-value pair to global maps new_map['global'][new_key] = new_value # If adding the unsigned_tx, parse it as a Tx object from bitcoin_lib # and count the number of inputs and outputs if new_key == PSBT_GLOBAL_UNSIGNED_TX: unsigned_tx_obj = Tx.parse(BytesIO(new_value)) num_inputs = len(unsigned_tx_obj.tx_ins) num_outputs = len(unsigned_tx_obj.tx_outs) # Set the amount of input and output maps that are expected [new_map['inputs'].append({}) for _ in range(num_inputs)] [new_map['outputs'].append({}) for _ in range(num_outputs)] # Parse each input key-value map elif num_inputs > 0: # If a separator has been reached, the new_key will be None # thus marking the end of that input if new_key == None: num_inputs -= 1 continue # curr_index is the position of the current input being parsed in the # list of inputs in new_map['inputs'] # Determined by the absolute value of the total number of inputs left # to parse - the total number of inputs curr_index = abs(num_inputs - len(new_map['inputs'])) new_map['inputs'][curr_index][new_key] = new_value # Parse each output key-value map elif num_outputs > 0: # If a separator has been reached, the new_key will be None marking # the end of that output if new_key == None: num_outputs -= 1 continue # curr_index is the position of the current output being parsed # in the list of inputs in new_map['outputs'] # Determined by the absolute value of the total number of inputs left # to parse - the total number of inputs curr_index = abs(num_outputs - len(new_map['outputs'])) new_map['outputs'][curr_index][new_key] = new_value return cls(dict_of_maps=new_map)