Skip to content

Commit

Permalink
add ability to create coinjoin like tx for a wallet, add method to an…
Browse files Browse the repository at this point in the history
…alyze annominity set of a tx for a user
  • Loading branch information
Jwyman328 committed Nov 8, 2024
1 parent ef79eb6 commit d45432a
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 26 deletions.
19 changes: 14 additions & 5 deletions backend/src/controllers/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
ValidationErrorResponse,
SimpleErrorResponse,
)
from src.testbridge.wallet_spends import create_and_broadcast_transaction_for_bdk_wallet
from src.testbridge.wallet_spends import (
create_and_broadcast_transaction_for_bdk_wallet,
create_and_broadcast_coinjoin_for_bdk_wallet,
)

from src.services.wallet.raw_output_script_examples import (
p2pkh_raw_output_script,
Expand Down Expand Up @@ -170,7 +173,8 @@ def create_spendable_wallet():
Then give the wallet a few more UTXOs.
"""
try:
data = CreateSpendableWalletRequestDto.model_validate_json(request.data)
data = CreateSpendableWalletRequestDto.model_validate_json(
request.data)

bdk_network: bdk.Network = bdk.Network.__members__[data.network]

Expand All @@ -185,7 +189,8 @@ def create_spendable_wallet():

if wallet_descriptor is None:
return (
SimpleErrorResponse(message="Error creating wallet").model_dump(),
SimpleErrorResponse(
message="Error creating wallet").model_dump(),
400,
)

Expand Down Expand Up @@ -222,8 +227,12 @@ def create_spendable_wallet():
)
mine_a_block_to_miner()
wallet.sync(blockchain, None)
create_and_broadcast_transaction_for_bdk_wallet(
wallet, blockchain, 50000, 10, p2wsh_raw_output_script
create_and_broadcast_coinjoin_for_bdk_wallet(
wallet,
blockchain,
50000,
10,
[p2wsh_raw_output_script, p2pkh_raw_output_script],
)

# Fund the wallet again so that there are a bunch of utxos
Expand Down
112 changes: 91 additions & 21 deletions backend/src/services/privacy_metrics/privacy_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

import structlog

from src.services.wallet.wallet import WalletService

LOGGER = structlog.get_logger()


Expand All @@ -30,8 +32,8 @@ def analyze_tx_privacy(
transaction = WalletService.get_transaction(txid)
for privacy_metric in privacy_metrics:
if privacy_metric == PrivacyMetricName.ANNOMINITY_SET:
mock_set = 5
result = cls.analyze_annominit_set(txid, mock_set)
mock_set = 2
result = cls.analyze_annominity_set(transaction, mock_set)
results[privacy_metric] = result

elif privacy_metric == PrivacyMetricName.NO_ADDRESS_REUSE:
Expand Down Expand Up @@ -106,8 +108,69 @@ def analyze_tx_privacy(
return results

@classmethod
def analyze_annominit_set(cls, txid: str, desired_annominity_set: int) -> bool:
return True
def analyze_annominity_set(
cls,
transaction: Optional[Transaction],
desired_annominity_set: int = 2,
allow_some_uneven_change: bool = True,
) -> bool:
"""Analyze if the users output/s in the tx have the desired annominity
set.
If allow_some_uneven_change is True, then the privacy metric will pass
if atleast one of the user's outputs is above the
desired annominity set.
If allow_some_uneven_change is False, then the privacy metric will
only pass if all of the user's outputs are above the desired
annominity set.
"""
if transaction is None:
return False
# compare the users utxos to other utxos,
# if other utxos do not have the same value
# then this fails the annominity set metric

# Check that all outputs have already been fetched recently
cls.ensure_recently_fetched_outputs()

output_annominity_count = WalletService.calculate_output_annominity_sets(
transaction.outputs
)
users_utxos_that_passed_annominity_test = 0
users_utxos_that_failed_annominity_test = 0
for output in transaction.outputs:
user_output = WalletService.get_output_from_db(
transaction.txid, output.output_n
)
is_users_output = user_output is not None
if is_users_output:
annominity_set = output_annominity_count[output.value]
if annominity_set < desired_annominity_set:
# the users output has a lower annominity set
# than the desired annominity set
# therefore this metric fails
users_utxos_that_failed_annominity_test += 1
else:
users_utxos_that_passed_annominity_test += 1

if users_utxos_that_passed_annominity_test == 0:
return False

if allow_some_uneven_change is False:
# if the user has any utxos that failed the annominity test
# then this metric fails
if users_utxos_that_failed_annominity_test > 0:
return False
else:
# all utxos passed the annominity test
return True
else: # allow_some_uneven_change is True
# at this point we know that the user has at least one utxo
# that passed the annominity test, if the user has other utxos
# that do not have an annominity set above 0 we just consider it part of the
# uneven change that is allowed.
return True

@classmethod
def analyze_no_address_reuse(
Expand All @@ -117,25 +180,9 @@ def analyze_no_address_reuse(
# can I inject this in?
# circular imports are currently preventing it.
from src.services.wallet.wallet import WalletService
from src.services.last_fetched.last_fetched_service import LastFetchedService

# Check that all outputs have already been fetched recently
last_fetched_output_datetime = (
LastFetchedService.get_last_fetched_output_datetime()
)
now = datetime.now()
refetch_interval = timedelta(minutes=5)

should_refetch_outputs = (
now - last_fetched_output_datetime > refetch_interval
if last_fetched_output_datetime is not None
else True # if last_fetched_output_datetime is None, we should "fetch" for first time
)

if last_fetched_output_datetime is None or should_refetch_outputs:
LOGGER.info("No last fetched output datetime found, fetching all outputs")
# this will get all the outputs and add them to the database, ensuring that they exist
WalletService.get_all_outputs()
cls.ensure_recently_fetched_outputs()
outputs = OutputModel.query.filter_by(txid=txid).all()
for output in outputs:
if WalletService.is_address_reused(output.address):
Expand Down Expand Up @@ -299,3 +346,26 @@ def analyze_no_post_mix_change(cls, txid: str) -> bool:
@classmethod
def analyze_segregate_postmix_and_nonmix(cls, txid: str) -> bool:
return True

@classmethod
def ensure_recently_fetched_outputs(cls) -> None:
from src.services.last_fetched.last_fetched_service import LastFetchedService

# Check that all outputs have already been fetched recently
last_fetched_output_datetime = (
LastFetchedService.get_last_fetched_output_datetime()
)
now = datetime.now()
refetch_interval = timedelta(minutes=5)

should_refetch_outputs = (
now - last_fetched_output_datetime > refetch_interval
if last_fetched_output_datetime is not None
else True # if last_fetched_output_datetime is None, we should "fetch" for first time
)

if last_fetched_output_datetime is None or should_refetch_outputs:
LOGGER.info(
"No last fetched output datetime found, fetching all outputs")
# this will get all the outputs and add them to the database, ensuring that they exist
WalletService.get_all_outputs()
7 changes: 7 additions & 0 deletions backend/src/services/wallet/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,13 @@ def populate_outputs_and_labels(
LOGGER.error("Error populating outputs and labels", error=e)
DB.session.rollback()

@classmethod
def get_output_from_db(
cls, txid: str, vout: int
) -> Optional[OutputModel]:
"""Get an output from the database by the txid and vout."""
output = OutputModel.query.filter_by(txid=txid, vout=vout).first()
return output
# TODO should this even go here or in its own service?
@classmethod
def add_label_to_output(
Expand Down
61 changes: 61 additions & 0 deletions backend/src/testbridge/wallet_spends.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,64 @@ def create_and_broadcast_transaction_for_bdk_wallet(
LOGGER.error(
"Failed to sign the transaction, therefore it can not be broadcast"
)


def create_and_broadcast_coinjoin_for_bdk_wallet(
wallet: bdk.Wallet,
blockchain: bdk.Blockchain,
amount: int = 50000,
sats_per_vbyte: int = 10,
raw_output_scripts: list[str] = [p2sh_raw_output_script],
):
"""Create, sign and broadcast a coinjoin like transaction for the given bdk wallet
Send an equal amount to each of the given raw_output_scripts which will represent
wallets other than the user's wallet.
Send an equal amount to the user's wallet 3 times (hard coded below)
TODO make it so you can dynamically specificy how many equal outputs the user should have.
FYI there will also be a change output that is not of equal value to a change address
for the user's wallet.
"""
wallets_address_1 = wallet.get_address(bdk.AddressIndex.LAST_UNUSED())

user_wallet_script_1: bdk.Payload = wallets_address_1.address.script_pubkey()

wallets_address_2 = wallet.get_address(bdk.AddressIndex.LAST_UNUSED())

user_wallet_script_2: bdk.Payload = wallets_address_2.address.script_pubkey()

wallets_address_3 = wallet.get_address(bdk.AddressIndex.LAST_UNUSED())

user_wallet_script_3: bdk.Payload = wallets_address_3.address.script_pubkey()

tx_builder = bdk.TxBuilder()

utxos = wallet.list_unspent()
# I have no idea how to not select the utxos manually
# therefore just use all the utxos for now.
outpoints = [utxo.outpoint for utxo in utxos]
tx_builder = tx_builder.add_utxos(outpoints)

tx_builder = tx_builder.fee_rate(sats_per_vbyte)

for raw_output_script in raw_output_scripts:
binary_script = bytes.fromhex(raw_output_script)
script = bdk.Script(binary_script)
tx_builder = tx_builder.add_recipient(script, amount)

tx_builder = tx_builder.add_recipient(user_wallet_script_1, amount)
tx_builder = tx_builder.add_recipient(user_wallet_script_2, amount)
tx_builder = tx_builder.add_recipient(user_wallet_script_3, amount)

built_transaction: bdk.TxBuilderResult = tx_builder.finish(wallet)
signed = wallet.sign(built_transaction.psbt, sign_options=None)
if signed:
transaction = built_transaction.psbt.extract_tx()
LOGGER.info(f"broadcasting {built_transaction.transaction_details}")
blockchain.broadcast(transaction)
else:
LOGGER.error(
"Failed to sign the transaction, therefore it can not be broadcast"
)

0 comments on commit d45432a

Please sign in to comment.