Exemple #1
0
    def rebuild_input_states(self, key_images: List[Dict[str, Any]]) -> None:
        """Marks spent inputs as spent."""

        for image in key_images:
            if self.rpc.is_key_image_spent(bytes.fromhex(image["image"])):
                self.inputs[OutputIndex(bytes.fromhex(
                    image["hash"]), image["index"])].state = InputState.Spent
Exemple #2
0
    def send(self, address: Address, amount: int) -> bytes:
        """Provide the specified address with the specified amount."""

        # Update the available inputs.
        self.poll_blocks()

        # Prepare the spend.
        context: Dict[str,
                      Any] = self.watch.prepare_send(address, amount,
                                                     ATOMIC_XMR // 10)

        # Mark the spent inputs as spent in our copy of inputs.
        for input_i in context["inputs"]:
            self.inputs[OutputIndex(bytes.fromhex(input_i["hash"]),
                                    input_i["index"])].state = InputState.Spent

        # Sign it.
        publishable: List[str] = json.loads(
            json.dumps(self.wallet.sign(json.loads(json.dumps(context)))))

        # Publish it.
        self.watch.finalize_send(True, context, publishable[1])

        # Verify the WatchWallet's inputs equals our list.
        self.verify_inputs()

        # Wait for the outputs to unlock.
        self.wait_for_unlock()

        # Return the hash.
        return bytes.fromhex(publishable[0])
    def from_json(output: Dict[str, Any]) -> Any:
        """Load an OutputInfo from JSON."""

        result: Any = OutputInfo(
            OutputIndex(bytes.fromhex(output["hash"]), output["index"]),
            output["timelock"],
            output["amount"],
            bytes.fromhex(output["spend_key"]),
        )
        result.state = InputState(output["state"])
        return result
Exemple #4
0
    def can_spend(
        self,
        tx: Transaction,
    ) -> Tuple[List[bytes], Dict[OutputIndex, OutputInfo]]:
        """
        Returns the found payment IDs and spendable outputs (OutputInfos indexed by OutputIndexes).

        The output destination must be determined through looking up the unique factor.
        This is the payment ID on payment ID networks, although the amount of IDs is not guaranteed.
        Any amount other than one causes the deposit destination to not be determinable.

        On subaddress networks, the unique factor is the spend key contained in the OutputInfo.
        """

        # Create the shared keys.
        shared_keys: List[bytes] = []
        for R in tx.Rs:
            shared_keys.append(
                self.crypto.create_shared_key(self.private_view_key, R))

        # Get the payment IDs.
        payment_IDs: List[bytes] = self.crypto.get_payment_IDs(
            shared_keys, tx.payment_IDs)

        # Found spendable outputs.
        spendable_outputs: Set[int] = set()

        # Result.
        result: Dict[OutputIndex, OutputInfo] = {}

        for shared_key in shared_keys:
            # Check each output unless it's already been found spendable.
            # Quality optimization due to how slow Python ed25519 is.
            # Also necessary to stop exploits based on R reuse (handled elsewhere) and torsion points (not handled elsewhere).
            for o in range(len(tx.outputs)):
                if o in spendable_outputs:
                    continue

                can_spend_res: Optional[
                    OutputInfo] = self.crypto.can_spend_output(
                        self.unique_factors, shared_key, tx, o)
                if can_spend_res is not None:
                    spendable_outputs.add(o)
                    result[OutputIndex(tx.tx_hash, o)] = can_spend_res

        # Merge the new TXOs into the Wallet's TXOs.
        for txo in result:
            if txo in self.inputs:
                continue

            self.inputs[txo] = result[txo]

        # Return the payment IDs + result.
        return (payment_IDs, result)
    def from_json(output: Dict[str, Any]) -> Any:
        """Load an MoneroOutputInfo from JSON."""

        result: Any = MoneroOutputInfo(
            OutputIndex(bytes.fromhex(output["hash"]), output["index"]),
            output["timelock"],
            output["amount"],
            bytes.fromhex(output["spend_key"]),
            (output["subaddress"][0], output["subaddress"][1]),
            bytes.fromhex(output["amount_key"]),
            bytes.fromhex(output["commitment"]),
        )
        result.state = InputState(output["state"])
        return result
Exemple #6
0
    def finalize_send(self, success: bool, context: Dict[str, Any],
                      serialization: str) -> bool:
        """
        Publishes a signed transaction if success is true.
        Marks used inputs as spent if the transaction is successfully published.
        Else, marks used inputs as spendable.
        """

        if success:
            try:
                self.rpc.publish_transaction(bytes.fromhex(serialization))
            except RPCError:
                success = False

        state = InputState.Spent if success else InputState.Spendable
        for input_i in context["inputs"]:
            self.inputs[OutputIndex(bytes.fromhex(input_i["hash"]),
                                    input_i["index"])].state = state
        return success
    def can_spend_output(
        self,
        unique_factors: Dict[bytes, Tuple[int, int]],
        shared_key: bytes,
        tx: Transaction,
        o: int,
    ) -> Optional[MoneroOutputInfo]:
        """Checks if an output is spendable and returns the relevant info."""

        # Grab the output.
        output = tx.outputs[o]

        # Transaction one time keys are defined as P = Hs(H8Ra || i)G + B.
        # This is rewrittable as B = P - Hs(8Ra || i) G.

        # Hs(8Ra || i)
        amount_key: bytes = ed.Hs(shared_key + to_var_int(o))

        # P - Hs(8Ra || i)G
        amount_key_G: ed.CompressedPoint = ed.scalarmult(
            ed.B, ed.decodeint(amount_key))
        # Make it negative so it can be subtracted by adding it.
        amount_key_G = (-amount_key_G[0], amount_key_G[1])
        spend_key: bytes = ed.encodepoint(
            ed.add_compressed(ed.decodepoint(output.key), amount_key_G))

        # We now have the spend key of the Transaction.
        if spend_key in unique_factors:
            # Get the amount.
            amount: int = 0
            if isinstance(output, MinerOutput):
                amount = output.amount
            else:
                # Decrypt the amount.
                amount = int.from_bytes(
                    output.amount, byteorder="little") ^ int.from_bytes(
                        ed.H(b"amount" + amount_key)[0:8], byteorder="little")

            commitment: bytes = ed.COMMITMENT_MASK
            if isinstance(output, Output):
                # The encrypted amount is malleable.
                # We need to rebuild the commitment to verify it's accurate.
                commitment = ed.Hs(b"commitment_mask" + amount_key)
                if (ed.encodepoint(
                        ed.add_compressed(
                            ed.scalarmult(
                                ed.B,
                                ed.decodeint(commitment),
                            ),
                            ed.scalarmult(ed.C, amount),
                        )) != output.commitment):
                    return None

            return MoneroOutputInfo(
                OutputIndex(tx.tx_hash, o),
                tx.unlock_time,
                amount,
                spend_key,
                (unique_factors[spend_key][0], unique_factors[spend_key][1]),
                amount_key,
                commitment,
            )
        return None
Exemple #8
0
    def prepare_send(
        self,
        dest: Address,
        amount: int,
        fee: int,
        minimum_input: int = 0,
        inputs_override: Optional[Dict[OutputIndex, OutputInfo]] = None,
    ) -> Dict[str, Any]:
        """
        Prepares a send to the destination for the specified amount with the specified fee.
        Skip inputs with a value less than minimum_input.
        If inputs are passed in, those inputs are used.
        Else, the WatchWallet's inputs are used.
        The selected inputs from the list of inputs have their state updated to Transmitted.

        Creates two outputs: one to the destination and a change address.
        The change address is the root view and spend key as a standard address.
        Also finds valid mixins and gets the needed information surrounding them.

        Returns the context to be passed to the cold wallet's sign.

        Raises BalanceError if there isn't enough of a balance to cover the transaction.
        Raises MixinError if there aren't enough mixins to use.
        Raises FeeError if the the fee is too low.
        """

        # Grab the inputs to use.
        inputs: Dict[OutputIndex, OutputInfo] = self.inputs
        if inputs_override is not None:
            inputs = inputs_override

        # Create the context.
        context: Dict[str, Any] = {
            "inputs": [],
            "mixins": [],
            "ring": [],
            "outputs": [],
            "fee": fee,
        }

        # Grab the height:
        height: int = self.rpc.get_block_count()

        # Grab the newest TXO.
        newest_unlocked_block: Block = self.rpc.get_block(
            self.rpc.get_block_hash(height - self.crypto.miner_lock_blocks))
        newest_tx: bytes = newest_unlocked_block.header.miner_tx_hash
        if newest_unlocked_block.hashes:
            newest_tx = newest_unlocked_block.hashes[-1]
        newest_txo: int = self.rpc.get_o_indexes(newest_tx)[-1]

        # Check there's enough mixins available.
        mixins_available: int = newest_txo - self.crypto.oldest_txo
        if mixins_available < self.crypto.required_mixins:
            raise MixinError("Not enough mixins available.")
        mixin_bytes: int = ((mixins_available.bit_length() // 8) + 1) * 8

        # Needed transaction value.
        value: int = amount + fee
        for index in inputs:
            if (
                    # Skip the Input if it's not spendable.
                (inputs[index].state != InputState.Spendable) or
                    # Skip the Input if it's timelocked.
                (inputs[index].timelock >= height) or
                    # Skip the Input if it's dust.
                (inputs[index].amount < minimum_input)):
                continue

            # Add the input.
            context["inputs"].append(inputs[index].to_json())

            # Grab mixins.
            # Start by getting and adding the Input's actual index.
            actual: int = self.rpc.get_o_indexes(
                inputs[index].index.tx_hash)[inputs[index].index.index]
            context["mixins"].append([actual])

            # Add the other mixins.
            while len(context["mixins"][-1]) != self.crypto.required_mixins:
                new_mixin = self.crypto.oldest_txo + (
                    int.from_bytes(urandom(mixin_bytes), byteorder="little") %
                    mixins_available)
                if new_mixin in context["mixins"][-1]:
                    continue
                context["mixins"][-1].append(new_mixin)

            # Sort the mixins.
            context["mixins"][-1].sort()
            # Specify the input's index to the mixins.
            context["inputs"][-1]["mixin_index"] = context["mixins"][-1].index(
                actual)

            # Add the ring info..
            context["ring"].append([])
            for m in range(len(context["mixins"][-1])):
                outs: Dict[str,
                           Any] = self.rpc.get_outs(context["mixins"][-1][m])
                context["ring"][-1].append(
                    [outs["key"].hex(), outs["mask"].hex()])

            # Subtract the amount from the needed value.
            value -= inputs[index].amount

            # Break if we have enough funds.
            if value <= 0:
                break

        # Make sure we have enough funds.
        if value > 0:
            raise BalanceError(
                "Didn't have enough of a balance to cover the transaction.")

        # Make sure the fee is high enough.
        if fee < self.crypto.get_minimum_fee(
                self.rpc.get_fee_estimate(),
                len(context["inputs"]),
                2,
                context["mixins"],
                255,
                fee,
        ):
            raise Exception(FeeError, "Fee is too low.")

        # Mark the inputs as transmitted.
        for input in context["inputs"]:
            inputs[OutputIndex(bytes.fromhex(input["hash"]),
                               input["index"])].state = InputState.Transmitted

        # Add the output.
        address: Address = dest
        context["outputs"].append({
            "address": address.address,
            "amount": amount
        })

        # Return the context and amount of used Transactions.
        return context