def broadcast_next_transaction(internal_id): """ Broadcast a transaction, but only if it is one of the valid next transactions. """ transaction_store_filename = TRANSACTION_STORE_FILENAME initial_tx = load(transaction_store_filename=transaction_store_filename) recentdata = get_current_confirmed_transaction(initial_tx) internal_id = str(internal_id) internal_ids = [str(blah.internal_id) for blah in recentdata["next"]] if internal_id not in internal_ids: logger.error( f"Error: internal_id {internal_id} is an invalid next step") sys.exit(1) internal_map = dict([(str(blah.internal_id), blah) for blah in recentdata["next"]]) requested_tx = internal_map[str(internal_id)] bitcoin_transaction = requested_tx.bitcoin_transaction connection = get_bitcoin_rpc_connection() result = connection.sendrawtransaction(bitcoin_transaction) if type(result) == bytes: result = b2lx(result) logger.info("Broadcasted, txid: {}".format(result)) return result
def render_planned_tree_to_text_file(some_utxo, filename=TEXT_RENDERING_FILENAME): """ Dump some text describing the planned transaction tree to a text file. This is primarily for human debugging. """ logger.info("Rendering to text...") output = some_utxo.to_text() filename = TEXT_RENDERING_FILENAME fd = open(os.path.join(os.getcwd(), filename), "w") fd.write(output) fd.close() logger.info(f"Wrote to {filename}") return
def save(some_utxo, filename=TRANSACTION_STORE_FILENAME): """ Serialize the planned transaction tree (starting from some given planned output/UTXO) and then dump the serialization into json and write into a file. """ output_data = to_dict(some_utxo) output_json = json.dumps(output_data, sort_keys=False, indent=4, separators=(',', ': ')) with open(os.path.join(os.getcwd(), filename), "w") as fd: fd.write(output_json) logger.info(f"Wrote to {filename}")
def sign_planned_transactions(planned_transactions, parameters=None): logger.info("======== Start") # Finalize each transaction by creating a set of bitcoin objects (including # a bitcoin transaction) representing the planned transaction. for (counter, planned_transaction) in enumerate(planned_transactions): logger.info("--------") logger.info("current transaction name: {}".format( planned_transaction.name)) logger.info(f"counter: {counter}") sign_planned_transaction(planned_transaction, parameters=parameters)
def to_dict(self): """ Convert the current planned transaction to a formatted dictionary. """ data = { "counter": self.id, "internal_id": str(self.internal_id), "name": self.name, "txid": b2lx(self.bitcoin_transaction.GetTxid()), "inputs": dict([(idx, some_input.to_dict()) for (idx, some_input) in enumerate(self.inputs)]), "outputs": dict([(idx, some_output.to_dict()) for (idx, some_output) in enumerate(self.output_utxos)]), "bitcoin_transaction": b2x(self.bitcoin_transaction.serialize()), } if hasattr(self, "ctv_bitcoin_transaction"): logger.info("Transaction name: {}".format(self.name)) data["ctv_bitcoin_transaction"] = b2x(self.ctv_bitcoin_transaction.serialize()) data["ctv_bitcoin_transaction_txid"] = b2lx(self.ctv_bitcoin_transaction.GetTxid()) return data
def sign_planned_transaction(planned_transaction, parameters=None): """ Sign a planned transaction by parameterizing each of the witnesses based on the script templates from their predecesor coins. """ for planned_input in planned_transaction.inputs: logger.info("parent transaction name: {}".format( planned_input.utxo.transaction.name)) # Sanity test: all parent transactions should already be finalized assert planned_input.utxo.transaction.is_finalized == True planned_utxo = planned_input.utxo witness_template_selection = planned_input.witness_template_selection # sanity check if witness_template_selection not in planned_utxo.script_template.witness_templates.keys( ): raise VaultException( "UTXO {} is missing witness template \"{}\"".format( planned_utxo.internal_id, witness_template_selection)) witness_template = planned_utxo.script_template.witness_templates[ witness_template_selection] # Would use transaction.bitcoin_transaction.get_txid() but for the # very first utxo, the txid is going to be mocked for testing # purposes. So it's better to just use the txid property... txid = planned_utxo.transaction.txid vout = planned_utxo.vout relative_timelock = planned_input.relative_timelock if relative_timelock != None: # Note that it's not enough to just have the relative timelock # in the script; you also have to set it on the CTxIn object. planned_input.bitcoin_input = CTxIn(COutPoint(txid, vout), nSequence=relative_timelock) else: planned_input.bitcoin_input = CTxIn(COutPoint(txid, vout)) # TODO: is_finalized is misnamed here.. since the signature isn't # there yet. planned_input.is_finalized = True # Can't sign the input yet because the other inputs aren't finalized. # sanity check finalized = planned_transaction.check_inputs_outputs_are_finalized() assert finalized == True bitcoin_inputs = [ planned_input.bitcoin_input for planned_input in planned_transaction.inputs ] bitcoin_outputs = [ planned_output.bitcoin_output for planned_output in planned_transaction.output_utxos ] witnesses = [] planned_transaction.bitcoin_inputs = bitcoin_inputs planned_transaction.bitcoin_outputs = bitcoin_outputs # Must be a mutable transaction because the witnesses are added later. planned_transaction.bitcoin_transaction = CMutableTransaction( bitcoin_inputs, bitcoin_outputs, nLockTime=0, nVersion=2, witness=None) # python-bitcoin-utils had a bug where the witnesses weren't # initialized blank. #planned_transaction.bitcoin_transaction.witnesses = [] if len( bitcoin_inputs ) == 0 and planned_transaction.name != "initial transaction (from user)": raise VaultException("Can't have a transaction with zero inputs") # Now that the inputs are finalized, it should be possible to sign each # input on this transaction and add to the list of witnesses. witnesses = [] for planned_input in planned_transaction.inputs: # sign! # Make a signature. Use some code defined in the PlannedInput model. witness = parameterize_witness_template_by_signing( planned_input, parameters) witnesses.append(witness) # Now take the list of CScript objects and do the needful. ctxinwitnesses = [ CTxInWitness(CScriptWitness(list(witness))) for witness in witnesses ] witness = CTxWitness(ctxinwitnesses) planned_transaction.bitcoin_transaction.wit = witness planned_transaction.is_finalized = True if planned_transaction.name == "initial transaction (from user)": # serialization function fails, so just skip return serialized_transaction = planned_transaction.serialize() logger.info("tx len: {}".format(len(serialized_transaction))) logger.info("txid: {}".format( b2lx(planned_transaction.bitcoin_transaction.GetTxid()))) logger.info("Serialized transaction: {}".format( b2x(serialized_transaction)))
def parameterize_planned_utxo(planned_utxo, parameters=None): """ Parameterize a PlannedUTXO based on the runtime parameters. Populate and construct the output scripts based on the assigned script templates. """ script_template = planned_utxo.script_template miniscript_policy_definitions = script_template.miniscript_policy_definitions script = copy(planned_utxo.script_template.script_template) for some_variable in miniscript_policy_definitions.keys(): some_param = parameters[some_variable] if type(some_param) == dict: some_public_key = b2x(some_param["public_key"]) elif script_template == UserScriptTemplate and type( some_param) == str and some_variable == "user_key_hash160": some_public_key = some_param else: # some_param is already the public key some_public_key = b2x(some_param) script = script.replace("<" + some_variable + ">", some_public_key) # Insert the appropriate relative timelocks, based on the timelock # multiplier. relative_timelocks = planned_utxo.script_template.relative_timelocks timelock_multiplier = planned_utxo.timelock_multiplier if relative_timelocks not in [{}, None]: replacements = relative_timelocks["replacements"] # Update these values to take into account the timelock multiplier. replacements = dict((key, value * timelock_multiplier) for (key, value) in replacements.items()) # Insert the new value into the script. The value has to be # converted to the right value (vch), though. for (replacement_name, replacement_value) in replacements.items(): replacement_value = bitcoin.core._bignum.bn2vch(replacement_value) replacement_value = b2x(replacement_value) script = script.replace("<" + replacement_name + ">", replacement_value) # For testing later: # int.from_bytes(b"\x40\x38", byteorder="little") == 144*100 # b2x(bitcoin.core._bignum.bn2vch(144*100)) == "4038" # bitcoin.core._bignum.vch2bn(b"\x90\x00") == 144 # There might be other things in the script that need to be replaced. #script = script.replace("<", "") #script = script.replace(">", "") if "<" in script: raise VaultException("Script not finished cooking? {}".format(script)) # remove newlines script = script.replace("\n", " ") # reduce any excess whitespace while (" " * 2) in script: script = script.replace(" ", " ") # remove whitespace at the front, like for the cold storage UTXO script if script[0] == " ": script = script[1:] # remove trailing whitespace if script[-1] == " ": script = script[0:-1] # hack for python-bitcoinlib # see https://github.com/petertodd/python-bitcoinlib/pull/226 # TODO: this shouldn't be required anymore (v0.11.0 was released) script = script.replace("OP_CHECKSEQUENCEVERIFY", "OP_NOP3") # convert script into a parsed python object script = script.split(" ") script_items = [] for script_item in script: if script_item in bitcoin.core.script.OPCODES_BY_NAME.keys(): parsed_script_item = bitcoin.core.script.OPCODES_BY_NAME[ script_item] script_items.append(parsed_script_item) else: script_items.append(x(script_item)) p2wsh_redeem_script = CScript(script_items) scriptpubkey = CScript([OP_0, sha256(bytes(p2wsh_redeem_script))]) p2wsh_address = P2WSHBitcoinAddress.from_scriptPubKey(scriptpubkey) planned_utxo.scriptpubkey = scriptpubkey planned_utxo.p2wsh_redeem_script = p2wsh_redeem_script planned_utxo.p2wsh_address = p2wsh_address amount = planned_utxo.amount planned_utxo.bitcoin_output = CTxOut(amount, scriptpubkey) planned_utxo.is_finalized = True logger.info("UTXO name: {}".format(planned_utxo.name)) logger.info("final script: {}".format(script))
def initialize(private_key=None): """ Setup and initialize a new vault in the current working directory. This is the primary entrypoint for the prototype. """ check_vaultfile_existence() check_private_key_is_conformant(private_key) #amount = random.randrange(0, 100 * COIN) #amount = 7084449357 amount = 2 * COIN # TODO: A more sophisticated private key system is required, for any real # production use. some_private_keys = [CBitcoinSecret(private_key)] * 6 parameter_names = [ "user_key", "ephemeral_key_1", "ephemeral_key_2", "cold_key1", "cold_key2", "hot_wallet_key", ] parameters = { "num_shards": 5, "enable_burn_transactions": True, "enable_graphviz": True, "enable_graphviz_popup": False, "amount": amount, "unspendable_key_1": CPubKey( x("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" )), } for some_name in parameter_names: private_key = some_private_keys.pop() public_key = private_key.pub parameters[some_name] = { "private_key": private_key, "public_key": public_key } parameters["user_key_hash160"] = b2x( Hash160(parameters["user_key"]["public_key"])) # consistency check against required parameters required_parameters = ScriptTemplate.get_required_parameters() missing_parameters = False for required_parameter in required_parameters: if required_parameter not in parameters.keys(): logger.error(f"Missing parameter: {required_parameter}") missing_parameters = True if missing_parameters: logger.error("Missing parameters!") sys.exit(1) # connect to bitcoind (ideally, regtest) connection = get_bitcoin_rpc_connection() # setup the user private key (for P2WPKH) connection._call("importprivkey", str(parameters["user_key"]["private_key"]), "user") # Mine some coins into the "user_key" P2WPKH address #user_address = "bcrt1qrnwea7zc93l5wh77y832wzg3cllmcquqeal7f5" # parsed_address = P2WPKHBitcoinAddress(user_address) user_address = P2WPKHBitcoinAddress.from_scriptPubKey( CScript([OP_0, Hash160(parameters["user_key"]["public_key"])])) blocks = 110 if connection._call("getblockchaininfo")["blocks"] < blocks: try: connection._call("sendtoaddress", user_address, 50) except Exception: pass connection._call("generatetoaddress", blocks, str(user_address)) # Now find an unspent UTXO. unspent = connection._call("listunspent", 6, 9999, [str(user_address)], True, {"minimumAmount": amount / COIN}) if len(unspent) == 0: raise VaultException( "can't find a good UTXO for amount {}".format(amount)) # pick the first UTXO utxo_details = unspent[0] txid = utxo_details["txid"] # have to consume the whole UTXO amount = int(utxo_details["amount"] * COIN) initial_tx_txid = lx(utxo_details["txid"]) initial_tx = InitialTransaction(txid=initial_tx_txid) segwit_utxo = PlannedUTXO( name="segwit input coin", transaction=initial_tx, script_template=UserScriptTemplate, amount=amount, ) segwit_utxo._vout_override = utxo_details["vout"] initial_tx.output_utxos = [segwit_utxo] # for establishing vout # =============== # Here's where the magic happens. vault_initial_utxo = setup_vault(segwit_utxo, parameters) # =============== # Check that the tree is conforming to applicable rules. safety_check(segwit_utxo.transaction) # To test that the sharded UTXOs have the right amounts, do the following: # assert (second_utxo_amount * 99) + first_utxo_amount == amount # Display all UTXOs and transactions-- render the tree of possible # transactions. Mostly helpful for debugging purposes. render_planned_tree_to_text_file(segwit_utxo, filename=TEXT_RENDERING_FILENAME) # stats logger.info("*** Stats and numbers") logger.info( f"{PlannedUTXO.__counter__} UTXOs, {PlannedTransaction.__counter__} transactions" ) sign_transaction_tree(segwit_utxo, parameters) save(segwit_utxo) # TODO: Delete the ephemeral keys. # (graph generation can wait until after key deletion) if parameters["enable_graphviz"] == True: generate_graphviz(segwit_utxo, parameters, output_filename="output.gv") # Create another planned transaction tree this time using # OP_CHECKTEMPLATEVERIFY from bip119. This can be performed after key # deletion because OP_CTV standard template hashes are not based on keys # and signatures. make_planned_transaction_tree_using_bip119_OP_CHECKTEMPLATEVERIFY( initial_tx, parameters=parameters) save(segwit_utxo, filename="transaction-store.ctv.json") # A vault has been established. Write the vaultfile. make_vaultfile()
def bake_ctv_transaction(some_transaction, skip_inputs=False, parameters=None): """ Create a OP_CHECKTEMPLATEVERIFY version transaction for the planned transaction tree. This version uses a hash-based covenant opcode instead of using pre-signed transactions with trusted key deletion. This function does two passes over the planned transaction tree, consisting of (1) crawling the whole tree and generating standard template hashes (starting with the deepest elements in the tree and working backwards towards the root of the tree), and then (2) crawling the whole tree and assigning txids to the inputs. This is possible because OP_CHECKTEMPLATEVERIFY does not include the hash of the inputs in the standard template hash, otherwise there would be a recursive hash commitment dependency loop error. See the docstring for bake_ctv_output too. """ if hasattr(some_transaction, "ctv_baked") and some_transaction.ctv_baked == True: return some_transaction.ctv_bitcoin_transaction # Bake each UTXO. Recurse down the tree and compute StandardTemplateHash # values (to be placed in scriptpubkeys) for OP_CHECKTEMPLATEVERIFY. These # standard template hashes can only be computed once the descendant tree is # computed, so it must be done recursively. for utxo in some_transaction.output_utxos: bake_ctv_output(utxo, parameters=parameters) # Construct python-bitcoinlib bitcoin transactions and attach them to the # PlannedTransaction objects, once all the UTXOs are ready. logger.info("Baking a transaction with name {}".format( some_transaction.name)) bitcoin_inputs = [] if not skip_inputs: for some_input in some_transaction.inputs: # When computing the standard template hash for a child transaction, # the child transaction needs to be only "partially" baked. It doesn't # need to have the inputs yet. if some_input.utxo.transaction.__class__ == InitialTransaction or some_input.transaction.name == "Burn some UTXO": txid = some_input.utxo.transaction.txid else: logger.info("The parent transaction name is: {}".format( some_input.utxo.transaction.name)) logger.info("Name of the UTXO being spent: {}".format( some_input.utxo.name)) logger.info("Current transaction name: {}".format( some_input.transaction.name)) # This shouldn't happen... We should be able to bake transactions # in a certain order and be done with this. #if not hasattr(some_input.utxo.transaction, "ctv_bitcoin_transaction"): # bake_ctv_transaction(some_input.utxo.transaction, parameters=parameters) # TODO: this creates an infinite loop.... txid = some_input.utxo.transaction.ctv_bitcoin_transaction.GetTxid( ) vout = some_input.utxo.transaction.output_utxos.index( some_input.utxo) relative_timelock = None if some_input.utxo.script_template.__class__ in [ ColdStorageScriptTemplate, ShardScriptTemplate ]: # TODO: This should be controlled by the template or whole # program parameters. relative_timelock = 144 if relative_timelock: bitcoin_input = CTxIn(COutPoint(txid, vout), nSequence=relative_timelock) else: bitcoin_input = CTxIn(COutPoint(txid, vout)) bitcoin_inputs.append(bitcoin_input) bitcoin_outputs = [] for some_output in some_transaction.output_utxos: amount = some_output.amount # For certain UTXOs, just use the previous UTXO script templates, # instead of the CTV version. (utxo.ctv_bypass == True) if hasattr(some_output, "ctv_bypass"): scriptpubkey = some_output.scriptpubkey else: scriptpubkey = some_output.ctv_scriptpubkey bitcoin_output = CTxOut(amount, scriptpubkey) bitcoin_outputs.append(bitcoin_output) bitcoin_transaction = CMutableTransaction(bitcoin_inputs, bitcoin_outputs, nLockTime=0, nVersion=2, witness=None) if not skip_inputs: witnesses = [] for some_input in some_transaction.inputs: logger.info("Transaction name: {}".format(some_transaction.name)) logger.info("Spending UTXO with name: {}".format( some_input.utxo.name)) logger.info("Parent transaction name: {}".format( some_input.utxo.transaction.name)) if some_transaction.name in [ "Burn some UTXO", "Funding commitment transaction" ]: witness = some_input.witness else: witness = some_input.ctv_witness #logger.info("Appending witness: {}".format(list(witness))) witnesses.append(witness) ctxinwitnesses = [ CTxInWitness(CScriptWitness(list(witness))) for witness in witnesses ] witness = CTxWitness(ctxinwitnesses) bitcoin_transaction.wit = witness else: bitcoin_transaction.wit = CTxWitness() some_transaction.ctv_bitcoin_transaction = bitcoin_transaction if not skip_inputs: some_transaction.ctv_baked = True return bitcoin_transaction