Example #1
0
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
Example #2
0
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
Example #3
0
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}")
Example #4
0
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)
Example #5
0
    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
Example #6
0
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)))
Example #7
0
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))
Example #8
0
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()
Example #9
0
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