All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.bitcoinj.wallet.CoinJoinExtension Maven / Gradle / Ivy

There is a newer version: 21.1.2
Show newest version
/*
 * Copyright (c) 2023 Dash Core Group
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.bitcoinj.wallet;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.protobuf.CodedOutputStream;
import net.jcip.annotations.GuardedBy;
import org.bitcoinj.coinjoin.CoinJoin;
import org.bitcoinj.coinjoin.CoinJoinClientOptions;
import org.bitcoinj.coinjoin.CoinJoinConstants;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.BlockChain;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.KeyId;
import org.bitcoinj.core.StoredBlock;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.Utils;
import org.bitcoinj.crypto.ChildNumber;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.crypto.IDeterministicKey;
import org.bitcoinj.crypto.factory.ECKeyFactory;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptPattern;
import org.bitcoinj.utils.Threading;
import org.bouncycastle.crypto.params.KeyParameter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Stream;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.lang.Math.max;
import static org.bitcoinj.coinjoin.CoinJoinConstants.COINJOIN_EXTRA;
import static org.dashj.bls.Utils.HexUtils.HEX;

/**
 * Handles the CoinJoin related KeyChain
 */

public class CoinJoinExtension extends AbstractKeyChainGroupExtension {
    private static final Logger log = LoggerFactory.getLogger(CoinJoinExtension.class);
    private static final int COINJOIN_LOOKADHEAD = 400;
    private static final int COINJOIN_LOOKADHEAD_THRESHOLD = COINJOIN_LOOKADHEAD - 1;

    protected AnyKeyChainGroup coinJoinKeyChainGroup;

    protected int rounds = CoinJoinClientOptions.getRounds();

    private final ReentrantLock unusedKeysLock = Threading.lock("unusedKeysLock");
    @GuardedBy("unusedKeysLock")
    protected final HashMap unusedKeys = Maps.newHashMapWithExpectedSize(1024);
    // TODO: we may not need keyUsage, it is used as a way to audit unusedKeys
    @GuardedBy("unusedKeysLock")
    protected final HashMap keyUsage = Maps.newHashMap();
    private boolean loadedKeys = false;

    public CoinJoinExtension(Wallet wallet) {
        super(wallet);
    }

    /**
     * Returns a Java package/class style name used to disambiguate this extension from others.
     */
    @Override
    public String getWalletExtensionID() {
        return "org.dashj.wallet.coinjoin";
    }

    /**
     * If this returns true, the mandatory flag is set when the wallet is serialized and attempts to load it without
     * the extension being in the wallet will throw an exception. This method should not change its result during
     * the object's lifetime.
     */
    @Override
    public boolean isWalletExtensionMandatory() {
        return false;
    }

    @Override
    public boolean supportsBloomFilters() {
        return true;
    }

    @Override
    public boolean supportsEncryption() {
        return true;
    }

    /**
     * Returns bytes that will be saved in the wallet.
     */
    @Override
    public byte[] serializeWalletExtension() {
        try {
            Protos.CoinJoin.Builder builder = Protos.CoinJoin.newBuilder();
            List keys = coinJoinKeyChainGroup != null ? coinJoinKeyChainGroup.serializeToProtobuf() : Lists.newArrayList();
            builder.addAllKey(keys);
            builder.setRounds(rounds);
            Protos.CoinJoin coinJoinProto = builder.build();
            ByteArrayOutputStream output = new ByteArrayOutputStream();
            final CodedOutputStream codedOutput = CodedOutputStream.newInstance(output);
            coinJoinProto.writeTo(codedOutput);
            codedOutput.flush();
            return output.toByteArray();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * Loads the contents of this object from the wallet.
     *
     * @param containingWallet the wallet to deserialize
     * @param data the serialized data
     */
    @Override
    public void deserializeWalletExtension(Wallet containingWallet, byte[] data) throws Exception {
        Protos.CoinJoin coinJoinProto = Protos.CoinJoin.parseFrom(data);
        if (containingWallet.isEncrypted()) {
            coinJoinKeyChainGroup = AnyKeyChainGroup.fromProtobufEncrypted(containingWallet.params,
                    coinJoinProto.getKeyList(), containingWallet.getKeyCrypter(), ECKeyFactory.get(), false);
        } else {
            coinJoinKeyChainGroup = AnyKeyChainGroup.fromProtobufUnencrypted(containingWallet.params,
                    coinJoinProto.getKeyList(), ECKeyFactory.get(), false);
        }
        if (coinJoinKeyChainGroup.hasKeyChains()) {
            setLookaheadSize();
        }
        rounds = coinJoinProto.getRounds();
        CoinJoinClientOptions.setRounds(rounds);
        loadedKeys = true;
    }

    private void setLookaheadSize() {
        coinJoinKeyChainGroup.getActiveKeyChain().setLookaheadSize(COINJOIN_LOOKADHEAD);
        coinJoinKeyChainGroup.getActiveKeyChain().setLookaheadThreshold(COINJOIN_LOOKADHEAD_THRESHOLD);
    }

    public boolean hasKeyChain(ImmutableList path) {
        if (coinJoinKeyChainGroup == null)
            return false;
        boolean hasPath = false;
        for (AnyDeterministicKeyChain chain : coinJoinKeyChainGroup.getDeterministicKeyChains()) {
            if (chain.getAccountPath().equals(path)) {
                hasPath = true;
                break;
            }
        }
        return hasPath;
    }

    public void addKeyChain(DeterministicSeed seed, ImmutableList path) {
        checkState(!seed.isEncrypted());
        if (!hasKeyChain(path)) {
            if (coinJoinKeyChainGroup == null) {
                coinJoinKeyChainGroup = AnyKeyChainGroup.builder(wallet.getParams(), ECKeyFactory.get()).build();
            }
            coinJoinKeyChainGroup.addAndActivateHDChain(AnyDeterministicKeyChain.builder().seed(seed).accountPath(path).build());
            setLookaheadSize();
        }
    }

    public void addEncryptedKeyChain(DeterministicSeed seed, ImmutableList path, @Nonnull KeyParameter keyParameter) {
        checkNotNull(keyParameter);
        checkState(seed.isEncrypted());
        if (!hasKeyChain(path)) {
            if (coinJoinKeyChainGroup == null) {
                coinJoinKeyChainGroup = AnyKeyChainGroup.builder(wallet.getParams(), ECKeyFactory.get()).build();
            }
            if (seed.isEncrypted()) {
                seed = seed.decrypt(wallet.getKeyCrypter(), "", keyParameter);
            }
            AnyDeterministicKeyChain chain = AnyDeterministicKeyChain.builder().seed(seed).accountPath(path).build();
            AnyDeterministicKeyChain encryptedChain = chain.toEncrypted(wallet.getKeyCrypter(), keyParameter);
            coinJoinKeyChainGroup.addAndActivateHDChain(encryptedChain);
            setLookaheadSize();
        }
    }

    @Override
    public AnyKeyChainGroup getKeyChainGroup() {
        return coinJoinKeyChainGroup;
    }

    public void setRounds(int rounds) {
        this.rounds = rounds;
    }

    public Coin getUnmixableTotal() {
        Coin sum = Coin.ZERO;
        getOutputs().get(-1).forEach(outPoint -> {
            if (((WalletEx) wallet).getRealOutpointCoinJoinRounds(outPoint.getOutPointFor()) == -2)
                sum.add(outPoint.getValue());
        });
        return sum;
    }

    /** returns a tree associating denominations with their outputs
     * Denomination of -1 are collaterals
     * Denomination of -2 are other undenominated outputs
     *
     * @return
     */
    public TreeMap> getOutputs() {
        checkNotNull(wallet);
        TreeMap> outputs = Maps.newTreeMap();
        for (Coin amount : CoinJoin.getStandardDenominations()) {
            outputs.put(CoinJoin.amountToDenomination(amount), Lists.newArrayList());
        }
        outputs.put(-2, Lists.newArrayList());
        outputs.put(0, Lists.newArrayList());
        for (TransactionOutput output : wallet.getUnspents()) {
            byte [] pkh = ScriptPattern.extractHashFromP2PKH(output.getScriptPubKey());
            if (getKeyChainGroup().findKeyFromPubKeyHash(pkh, Script.ScriptType.P2PKH) != null) {
                int denom = CoinJoin.amountToDenomination(output.getValue());
                List listDenoms = outputs.get(denom);
                listDenoms.add(output);
            } else {
                // non-denominated and non-collateral coins
                outputs.get(-2).add(output);
            }
        }
        return outputs;
    }

    @Override
    public String toString(boolean includeLookahead, boolean includePrivateKeys, @Nullable KeyParameter aesKey) {
        StringBuilder builder = new StringBuilder();
        builder.append("COINJOIN:\n Rounds: ").append(rounds).append("\n");
        builder.append(super.toString(includeLookahead, includePrivateKeys, aesKey)).append("\n");
        builder.append("Key Usage:").append(getKeyUsage()).append("\n");
        builder.append("Outputs:\n");

        for (Map.Entry> entry : getOutputs().entrySet()) {
            int denom = entry.getKey();
            List outputs = entry.getValue();
            Coin value = outputs.stream().map(TransactionOutput::getValue).reduce(Coin::add).orElse(Coin.ZERO);
            builder.append(CoinJoin.denominationToString(denom)).append(" outputs:").append(outputs.size()).append(" total:")
                    .append(value.toFriendlyString()).append("\n");
            outputs.forEach(output -> {
                TransactionOutPoint outPoint = new TransactionOutPoint(output.getParams(), output.getIndex(), output.getParentTransactionHash());
                builder.append("  addr:")
                        .append(Address.fromPubKeyHash(output.getParams(), ScriptPattern.extractHashFromP2PKH(output.getScriptPubKey())))
                        .append(" outpoint:")
                        .append(outPoint.toStringShort())
                        .append(" ");
                int rounds = ((WalletEx) wallet).getRealOutpointCoinJoinRounds(outPoint);
                builder.append(CoinJoin.getRoundsString(rounds));
                if (rounds >= 0) {
                    builder.append(" ").append(rounds).append(" rounds");
                    if (((WalletEx) wallet).isFullyMixed(outPoint)) {
                        builder.append(" (fully mixed)");
                    }
                } else {
                    builder.append(" ").append(output.getValue().toFriendlyString());
                }
                builder.append("\n");
            });
        }
        builder.append(getUnusedKeyReport());
        builder.append(getKeyUsageReport());

        return builder.toString();
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("COINJOIN:\n Rounds: ").append(rounds).append("\n");
        builder.append("Key Usage:").append(getKeyUsage()).append("\n");
        builder.append("Total Keys: ").append(getActiveKeyChain().issuedExternalKeys);
        return builder.toString();
    }

    @Override
    public boolean hasSpendableKeys() {
        return true;
    }

    @Override
    public boolean isTransactionRevelant(Transaction tx) {
        // use regular check based TransactionBag is* methods
        // there are no special transactions with CoinJoin
        return false;
    }

    /**
     *
     * @return the percentage of coinjoin keys used in transactions
     */
    public int getKeyUsage() {
        if (coinJoinKeyChainGroup.hasKeyChains()) {
            int totalKeys = coinJoinKeyChainGroup.getActiveKeyChain().getIssuedExternalKeys();
            List issuedKeys = coinJoinKeyChainGroup.getActiveKeyChain().getIssuedReceiveKeys();

            Set txes = wallet.getTransactions(true);

            Stream usedKeys = issuedKeys.stream().filter(key ->
                    txes.stream().anyMatch(tx ->
                            tx.getOutputs().stream().anyMatch(output -> {
                                if (ScriptPattern.isP2PKH(output.getScriptPubKey())) {
                                    byte[] publicKeyHash = ScriptPattern.extractHashFromP2PKH(output.getScriptPubKey());
                                    return Arrays.equals(publicKeyHash, key.getPubKeyHash());
                                } else return false;
                            })
                    )
            );

            return totalKeys > 0 ? (int) usedKeys.count() * 100 / totalKeys : 0;
        } else {
            return 0;
        }
    }

    public void addUnusedKey(DeterministicKey key) {
        unusedKeysLock.lock();
        try {
            unusedKeys.put(KeyId.fromBytes(key.getPubKeyHash()), key);
            keyUsage.put(key, false);
            log.info(COINJOIN_EXTRA, "adding unused key: {} / {} ", HEX.encode(key.getPubKeyHash()), key.getPath());
        } finally {
            unusedKeysLock.unlock();
        }
    }

    public void addUnusedKey(KeyId keyId) {
        unusedKeysLock.lock();
        try {
            DeterministicKey key = (DeterministicKey) findKeyFromPubKey(keyId.getBytes());
            if (key != null) {
                unusedKeys.put(KeyId.fromBytes(key.getPubKeyHash()), key);
                keyUsage.put(key, false);
                log.info(COINJOIN_EXTRA, "adding unused key: {} / {}", HEX.encode(key.getPubKeyHash()), key.getPath());
            } else {
                log.warn(COINJOIN_EXTRA, "cannot find {}", keyId);
            }
        } finally {
            unusedKeysLock.unlock();
        }
    }

    public DeterministicKey getUnusedKey() {
        unusedKeysLock.lock();
        try {
            if (unusedKeys.isEmpty()) {
                log.info(COINJOIN_EXTRA, "obtaining fresh key");
                log.info(COINJOIN_EXTRA, "keyUsage map has unused keys: {}, unused key count: {}", keyUsage.values().stream().noneMatch(used -> used), getUnusedKeyCount());
                return (DeterministicKey) freshReceiveKey();
            } else {
                DeterministicKey key = unusedKeys.values().stream().findFirst().get();
                log.info(COINJOIN_EXTRA, "reusing key: {} / {}", HEX.encode(key.getPubKeyHash()), key);
                log.info(COINJOIN_EXTRA, "keyUsage map says this key is used: {}, unused key count: {}", keyUsage.get(key), getUnusedKeyCount());

                // remove the key
                unusedKeys.remove(KeyId.fromBytes(key.getPubKeyHash()));
                keyUsage.put(key, true);

                return key;
            }
        } finally {
            unusedKeysLock.unlock();
        }
    }

    public void removeUnusedKey(KeyId keyId) {
        unusedKeysLock.lock();
        try {
            unusedKeys.remove(keyId);
            IDeterministicKey key = (IDeterministicKey) findKeyFromPubKeyHash(keyId.getBytes(), Script.ScriptType.P2PKH);
            keyUsage.put(key, true);
            log.info(COINJOIN_EXTRA, "remove unused key: {} / {}", HEX.encode(keyId.getBytes()), key);
        } finally {
            unusedKeysLock.unlock();
        }
    }

    @Override
    public void processTransaction(Transaction tx, StoredBlock block, BlockChain.NewBlockType blockType) {
        tx.getOutputs().forEach(output -> {
            if (ScriptPattern.isP2PKH(output.getScriptPubKey())) {
                byte[] pubKeyHash = ScriptPattern.extractHashFromP2PKH(output.getScriptPubKey());
                IDeterministicKey key = (IDeterministicKey) findKeyFromPubKeyHash(pubKeyHash, Script.ScriptType.P2PKH);
                if (loadedKeys) {
                    keyUsage.put(key, true);
                    unusedKeys.remove(KeyId.fromBytes(pubKeyHash));
                }
            }
        });
    }

    @Override
    public IDeterministicKey freshReceiveKey() {
        IDeterministicKey freshKey = super.freshReceiveKey();
        log.info(COINJOIN_EXTRA, "fresh key: {} / {}", HEX.encode(freshKey.getPubKeyHash()), freshKey);
        keyUsage.put(freshKey, true);
        return freshKey;
    }

    boolean isKeyUsed(byte[] pubKeyHash) {
        return wallet.getTransactions(false).stream().anyMatch(tx ->
                tx.getOutputs().stream().anyMatch(output -> {
                    if (ScriptPattern.isP2PKH(output.getScriptPubKey())) {
                        byte[] publicKeyHashFromTx = ScriptPattern.extractHashFromP2PKH(output.getScriptPubKey());
                        return Arrays.equals(publicKeyHashFromTx, pubKeyHash);
                    } else return false;
                })
        );
    }

    public void refreshUnusedKeys() {
        List issuedKeys;
        unusedKeysLock.lock();
        try {
            keyChainGroupLock.lock();
            try {
                unusedKeys.clear();
                issuedKeys = coinJoinKeyChainGroup.getActiveKeyChain().getIssuedReceiveKeys();
            } finally {
                keyChainGroupLock.unlock();
            }

            issuedKeys.forEach(key -> {
                unusedKeys.put(KeyId.fromBytes(key.getPubKeyHash()), (DeterministicKey) key);
                keyUsage.put(key, false);
            });

            Set txes = wallet.getTransactions(true);

            Stream usedKeys = issuedKeys.stream().filter(key -> {
                        boolean found = txes.stream().anyMatch(tx ->
                                tx.getOutputs().stream().anyMatch(output -> {
                                    if (ScriptPattern.isP2PKH(output.getScriptPubKey())) {
                                        byte[] publicKeyHash = ScriptPattern.extractHashFromP2PKH(output.getScriptPubKey());
                                        return Arrays.equals(publicKeyHash, key.getPubKeyHash());
                                    } else return false;
                                })
                        );
                        if (found) {
                            keyUsage.put(key, true);
                        }
                        return found;
                    }
            );

            usedKeys.forEach(key -> unusedKeys.remove(KeyId.fromBytes(key.getPubKeyHash())));

            unusedKeys.forEach((keyId, key) -> log.info(COINJOIN_EXTRA, "unused key: {}", key));
            keyUsage.forEach((key, used) -> {
                if (!used)
                    log.info(COINJOIN_EXTRA, "unused key: {}", key);
            });
            loadedKeys = true;
        } finally {
            unusedKeysLock.unlock();
        }
    }

    public String getUnusedKeyReport() {
        List issuedKeys;
        HashMap, IDeterministicKey> unusedKeyMap = Maps.newHashMap();
        unusedKeysLock.lock();
        try {
            keyChainGroupLock.lock();
            try {
                issuedKeys = coinJoinKeyChainGroup.getActiveKeyChain().getIssuedReceiveKeys();
            } catch (DeterministicUpgradeRequiredException e) {
                return "No unused keys\n";
            } finally {
                keyChainGroupLock.unlock();
            }

            issuedKeys.forEach(key -> unusedKeyMap.put(key.getPath(), key));

            Set txes = wallet.getTransactions(true);

            Stream usedKeys = issuedKeys.stream().filter(key ->
                    txes.stream().anyMatch(tx ->
                            tx.getOutputs().stream().anyMatch(output -> {
                                if (ScriptPattern.isP2PKH(output.getScriptPubKey())) {
                                    byte[] publicKeyHash = ScriptPattern.extractHashFromP2PKH(output.getScriptPubKey());
                                    return Arrays.equals(publicKeyHash, key.getPubKeyHash());
                                } else return false;
                            })
                    )
            );

            usedKeys.forEach(key -> unusedKeyMap.remove(key.getPath()));

            StringBuilder builder = new StringBuilder();
            Stream> sortedPaths = unusedKeyMap.keySet().stream().sorted((a, b) -> {
                int size1 = a.size();
                int size2 =  b.size();
                for (int i = 0; i < Math.min(size1, size2); i++) {
                    int comparison = a.get(i).compareTo(b.get(i));
                    if (comparison != 0) {
                        return comparison;
                    }
                }
                // If we haven't returned, the common prefix of both lists is identical.
                // The shorter list should be considered less than the longer one.
                return Integer.compare(size1, size2);
            });

            // add the sorted list of unused keys
            builder.append("Unused Key List: ");
            AtomicInteger largestGap = new AtomicInteger(0);
            AtomicInteger currentGap = new AtomicInteger(0);
            AtomicInteger lastIndex = new AtomicInteger(-1);
            sortedPaths.forEach(path -> {
                builder.append("  ").append(path).append("\n");
                int index = path.get(path.size() -1).i();
                if (lastIndex.get() != -1) {
                    if (lastIndex.get() + 1 == index) {
                        currentGap.getAndIncrement();
                    } else {
                        largestGap.set(max(largestGap.get(), currentGap.getAndSet(0)));
                    }
                } else {
                    currentGap.set(0);
                }
                lastIndex.set(index);
            });

            // add gap information
            builder.append("Largest Gap: ").append(largestGap.get()).append("\n");
            return builder.toString();

        } finally {
            unusedKeysLock.unlock();
        }
    }

    public String getKeyUsageReport() {
        List issuedKeys;
        HashMap usedKeyMap = Maps.newHashMap();
        unusedKeysLock.lock();
        try {
            keyChainGroupLock.lock();
            try {
                issuedKeys = coinJoinKeyChainGroup.getActiveKeyChain().getIssuedReceiveKeys();
            } catch (DeterministicUpgradeRequiredException e) {
                return "No keys reused\n";
            } finally {
                keyChainGroupLock.unlock();
            }

            Set txes = wallet.getTransactions(true);

            issuedKeys.forEach(key -> txes.forEach(tx -> {
                Stream keyUsage = tx.getOutputs().stream().filter(output -> {
                    if (ScriptPattern.isP2PKH(output.getScriptPubKey())) {
                        byte[] publicKeyHash = ScriptPattern.extractHashFromP2PKH(output.getScriptPubKey());
                        return Arrays.equals(publicKeyHash, key.getPubKeyHash());
                    } else return false;
                });
                int count = (int) keyUsage.count();
                if (count > 0) {
                    Integer currentCount = usedKeyMap.get((DeterministicKey) key);
                    usedKeyMap.put((DeterministicKey) key, currentCount == null ? 1 : currentCount + 1);
                }
            }));


            StringBuilder builder = new StringBuilder();
            builder.append("Duplicate Used Key List: \n");
            usedKeyMap.forEach((key, count) -> {
                if (count > 1)
                    builder.append("  ").append("hash160:").append(Utils.HEX.encode(key.getPubKeyHash())).append(":").append(count).append("\n");
            });
            return builder.toString();

        } finally {
            unusedKeysLock.unlock();
        }
    }

    public double getMixingProgress() {
        double requiredRounds = rounds + 0.875; // 1 x 50% + 1 x 50%^2 + 1 x 50%^3
        AtomicInteger totalInputs = new AtomicInteger();
        AtomicInteger totalRounds = new AtomicInteger();
        getOutputs().forEach((denom, outputs) -> {
            outputs.forEach(output -> {
                // do not count mixing collateral for fees
                if (denom >= 0) {
                    // getOutputs has a bug where non-denominated items are marked as denominated
                    TransactionOutPoint outPoint = new TransactionOutPoint(output.getParams(), output.getIndex(), output.getParentTransactionHash());
                    int rounds = ((WalletEx) wallet).getRealOutpointCoinJoinRounds(outPoint);
                    if (rounds >= 0) {
                        totalInputs.addAndGet(1);
                        totalRounds.addAndGet(rounds);
                    }
                } else if (denom == -2) {
                    // estimate what the denominations would be: use greedy algorithm
                    AtomicInteger unmixedInputs = new AtomicInteger(0);
                    AtomicReference outputValue = new AtomicReference<>(output.getValue().subtract(CoinJoin.getCollateralAmount()));
                    CoinJoinClientOptions.getDenominations().forEach(coin -> {
                        while (outputValue.get().subtract(coin).isGreaterThan(Coin.ZERO)) {
                            unmixedInputs.getAndIncrement();
                            outputValue.set(outputValue.get().subtract(coin));
                        }
                    });
                    totalInputs.set(totalInputs.get() + unmixedInputs.get());
                }
            });
        });
        double progress = totalInputs.get() != 0 ? totalRounds.get() / (requiredRounds * totalInputs.get()) : 0.0;
        log.info("getMixingProgress: {} = {} / ({} * {})", progress, totalRounds.get(), requiredRounds, totalInputs.get());
        return Math.max(0.0, Math.min(progress, 1.0));
    }

    public int getUnusedKeyCount() {
        return unusedKeys.size();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy