Remove UTXOMap and use TxoMap instead. Cleanup thread handling. Refactor parser and services. Remove RPC_WALLET_NOTIFICATION_PORT option.

This commit is contained in:
Manfred Karrer 2017-04-11 00:00:34 -05:00
parent c9917998d8
commit a24ca8edaf
23 changed files with 929 additions and 1070 deletions

View File

@ -77,7 +77,7 @@ public class BisqEnvironment extends StandardEnvironment {
private final String logLevel, providers;
private BitcoinNetwork bitcoinNetwork;
private final String btcNodes, seedNodes, ignoreDevMsg, useTorForBtc, rpcUser, rpcPassword,
rpcPort, rpcBlockNotificationPort, rpcWalletNotificationPort, dumpBlockchainData,
rpcPort, rpcBlockNotificationPort, dumpBlockchainData,
myAddress, banList, dumpStatistics, maxMemory, socks5ProxyBtcAddress,
socks5ProxyHttpAddress;
@ -181,9 +181,6 @@ public class BisqEnvironment extends StandardEnvironment {
rpcBlockNotificationPort = commandLineProperties.containsProperty(RpcOptionKeys.RPC_BLOCK_NOTIFICATION_PORT) ?
(String) commandLineProperties.getProperty(RpcOptionKeys.RPC_BLOCK_NOTIFICATION_PORT) :
"";
rpcWalletNotificationPort = commandLineProperties.containsProperty(RpcOptionKeys.RPC_WALLET_NOTIFICATION_PORT) ?
(String) commandLineProperties.getProperty(RpcOptionKeys.RPC_WALLET_NOTIFICATION_PORT) :
"";
dumpBlockchainData = commandLineProperties.containsProperty(RpcOptionKeys.DUMP_BLOCKCHAIN_DATA) ?
(String) commandLineProperties.getProperty(RpcOptionKeys.DUMP_BLOCKCHAIN_DATA) :
"";
@ -261,7 +258,6 @@ public class BisqEnvironment extends StandardEnvironment {
setProperty(RpcOptionKeys.RPC_PASSWORD, rpcPassword);
setProperty(RpcOptionKeys.RPC_PORT, rpcPort);
setProperty(RpcOptionKeys.RPC_BLOCK_NOTIFICATION_PORT, rpcBlockNotificationPort);
setProperty(RpcOptionKeys.RPC_WALLET_NOTIFICATION_PORT, rpcWalletNotificationPort);
setProperty(RpcOptionKeys.DUMP_BLOCKCHAIN_DATA, dumpBlockchainData);
setProperty(BtcOptionKeys.BTC_NODES, btcNodes);

View File

@ -169,9 +169,6 @@ public abstract class BisqExecutable {
parser.accepts(RpcOptionKeys.RPC_BLOCK_NOTIFICATION_PORT,
description("Bitcoind rpc port for block notifications", ""))
.withRequiredArg();
parser.accepts(RpcOptionKeys.RPC_WALLET_NOTIFICATION_PORT,
description("Bitcoind rpc port for wallet notifications", ""))
.withRequiredArg();
parser.accepts(RpcOptionKeys.DUMP_BLOCKCHAIN_DATA,
description("If set to true the blockchain data from RPC requests to Bitcoin Core are stored " +
"as json file in the data dir.", false))

View File

@ -17,51 +17,29 @@
package io.bisq.core.btc.wallet;
import io.bisq.core.dao.blockchain.BsqUTXO;
import io.bisq.core.dao.blockchain.BsqUTXOMap;
import io.bisq.core.dao.blockchain.TxOutputMap;
import lombok.extern.slf4j.Slf4j;
import org.bitcoinj.core.TransactionOutput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* We use a specialized version of the CoinSelector based on the DefaultCoinSelector implementation.
* We lookup for spendable outputs which matches our address of our address.
*/
@Slf4j
class BsqCoinSelector extends BisqDefaultCoinSelector {
private static final Logger log = LoggerFactory.getLogger(BsqCoinSelector.class);
private final Map<String, Set<BsqUTXO>> utxoSetByAddressMap = new HashMap<>();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
private TxOutputMap txOutputMap;
public BsqCoinSelector(boolean permitForeignPendingTx) {
super(permitForeignPendingTx);
}
public void setUtxoMap(BsqUTXOMap bsqUTXOMap) {
bsqUTXOMap.values().stream().forEach(utxo -> {
String address = utxo.getAddress();
if (!utxoSetByAddressMap.containsKey(address))
utxoSetByAddressMap.put(address, new HashSet<>());
utxoSetByAddressMap.get(address).add(utxo);
});
public void setTxoMap(TxOutputMap txOutputMap) {
this.txOutputMap = txOutputMap;
}
@Override
protected boolean isTxOutputSpendable(TransactionOutput output) {
if (WalletUtils.isOutputScriptConvertableToAddress(output)) {
return utxoSetByAddressMap.containsKey(WalletUtils.getAddressStringFromOutput(output));
} else {
log.warn("output.getScriptPubKey() not isSentToAddress or isPayToScriptHash");
return false;
}
return output.getParentTransaction() != null &&
txOutputMap.isTxOutputUnSpent(output.getParentTransaction().getHashAsString(), output.getIndex());
}
}

View File

@ -23,7 +23,7 @@ import io.bisq.core.btc.Restrictions;
import io.bisq.core.btc.exceptions.TransactionVerificationException;
import io.bisq.core.btc.exceptions.WalletException;
import io.bisq.core.dao.blockchain.BsqBlockchainManager;
import io.bisq.core.dao.blockchain.BsqUTXOMap;
import io.bisq.core.dao.blockchain.TxOutput;
import io.bisq.core.provider.fee.FeeService;
import io.bisq.core.user.Preferences;
import javafx.collections.FXCollections;
@ -42,6 +42,7 @@ import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.google.common.base.Preconditions.checkArgument;
@ -51,7 +52,7 @@ public class BsqWalletService extends WalletService {
private final BsqBlockchainManager bsqBlockchainManager;
private final BsqCoinSelector bsqCoinSelector;
@Getter
private final ObservableList<Transaction> walletBsqTransactions = FXCollections.observableArrayList();
private final ObservableList<Transaction> walletTransactions = FXCollections.observableArrayList();
private final CopyOnWriteArraySet<BsqBalanceListener> bsqBalanceListeners = new CopyOnWriteArraySet<>();
private Coin availableBsqBalance;
@ -77,6 +78,7 @@ public class BsqWalletService extends WalletService {
wallet.setCoinSelector(bsqCoinSelector);
wallet.addEventListener(walletEventListener);
wallet.addEventListener(new AbstractWalletEventListener() {
@Override
public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
@ -90,7 +92,7 @@ public class BsqWalletService extends WalletService {
@Override
public void onReorganize(Wallet wallet) {
updateWalletBsqTransactions();
updateBsqWalletTransactions();
}
@Override
@ -99,29 +101,25 @@ public class BsqWalletService extends WalletService {
@Override
public void onKeysAdded(List<ECKey> keys) {
updateWalletBsqTransactions();
updateBsqWalletTransactions();
}
@Override
public void onScriptsChanged(Wallet wallet, List<Script> scripts, boolean isAddingScripts) {
updateWalletBsqTransactions();
updateBsqWalletTransactions();
}
@Override
public void onWalletChanged(Wallet wallet) {
updateWalletBsqTransactions();
updateBsqWalletTransactions();
}
});
});
bsqBlockchainManager.addUtxoListener(bsqUTXOMap -> {
updateCoinSelector(bsqUTXOMap);
updateWalletBsqTransactions();
updateBsqBalance();
});
bsqBlockchainManager.getBsqTXOMap().addBurnedBSQTxMapListener(change -> {
updateWalletBsqTransactions();
bsqBlockchainManager.addTxOutputMapListener(bsqTxoMap -> {
bsqCoinSelector.setTxoMap(bsqTxoMap);
updateBsqWalletTransactions();
updateBsqBalance();
});
}
@ -144,6 +142,11 @@ public class BsqWalletService extends WalletService {
// Balance
///////////////////////////////////////////////////////////////////////////////////////////
private void updateBsqBalance() {
availableBsqBalance = bsqCoinSelector.select(NetworkParameters.MAX_MONEY, getMyUnspentBsqTxOutputsFromWallet()).valueGathered;
bsqBalanceListeners.stream().forEach(e -> e.updateAvailableBalance(availableBsqBalance));
}
@Override
public Coin getAvailableBalance() {
return availableBsqBalance;
@ -157,66 +160,56 @@ public class BsqWalletService extends WalletService {
bsqBalanceListeners.remove(listener);
}
private void updateBsqBalance() {
availableBsqBalance = bsqCoinSelector.select(NetworkParameters.MAX_MONEY, getMyBsqUtxosFromWallet()).valueGathered;
bsqBalanceListeners.stream().forEach(e -> e.updateAvailableBalance(availableBsqBalance));
}
///////////////////////////////////////////////////////////////////////////////////////////
// UTXO
// BSQ TransactionOutputs and Transactions
///////////////////////////////////////////////////////////////////////////////////////////
private void updateCoinSelector(BsqUTXOMap bsqUTXOMap) {
bsqCoinSelector.setUtxoMap(bsqUTXOMap);
private void updateBsqWalletTransactions() {
walletTransactions.setAll(getAllBsqTransactionsFromWallet());
}
private void updateWalletBsqTransactions() {
walletBsqTransactions.setAll(getWalletTransactionsWithBsqTxo());
}
private Set<TransactionOutput> getAllBsqUtxosFromWallet() {
return getTransactions(true).stream()
.flatMap(tx -> tx.getOutputs().stream())
.filter(out -> out.getParentTransaction() != null && bsqBlockchainManager.getBsqUTXOMap()
.containsTuple(out.getParentTransaction().getHashAsString(), out.getIndex()))
private Set<TransactionOutput> getAllBsqTxOutputsFromWallet() {
return getWalletTransactionOutputStream()
.filter(out -> out.getParentTransaction() != null && bsqBlockchainManager.getTxOutputMap()
.contains(out.getParentTransaction().getHashAsString(), out.getIndex()))
.collect(Collectors.toSet());
}
private Set<TransactionOutput> getMyBsqUtxosFromWallet() {
return getAllBsqUtxosFromWallet().stream()
.filter(out -> out.isMine(wallet))
.collect(Collectors.toSet());
}
private Set<TransactionOutput> getWalletBsqTxos() {
return getTransactions(true).stream()
.flatMap(tx -> tx.getOutputs().stream())
private Set<TransactionOutput> getAllUnspentBsqTxOutputsFromWallet() {
return getAllBsqTxOutputsFromWallet().stream()
.filter(out -> {
final Transaction parentTransaction = out.getParentTransaction();
if (parentTransaction == null)
final Transaction tx = out.getParentTransaction();
if (tx == null) {
return false;
final String id = parentTransaction.getHashAsString();
return bsqBlockchainManager.getBsqTXOMap().containsTuple(id, out.getIndex()) ||
bsqBlockchainManager.getBsqTXOMap().getBurnedBSQTxMap().containsKey(id);
} else {
final TxOutput txOutput = bsqBlockchainManager.getTxOutputMap()
.get(tx.getHashAsString(), out.getIndex());
return txOutput != null && txOutput.isUnSpend();
}
})
.collect(Collectors.toSet());
}
private Set<Transaction> getWalletTransactionsWithBsqUtxo() {
return getAllBsqUtxosFromWallet().stream()
.map(TransactionOutput::getParentTransaction)
private Set<TransactionOutput> getMyUnspentBsqTxOutputsFromWallet() {
return getAllUnspentBsqTxOutputsFromWallet().stream()
.filter(out -> out.isMine(wallet))
.collect(Collectors.toSet());
}
private Set<Transaction> getWalletTransactionsWithBsqTxo() {
return getWalletBsqTxos().stream()
private Stream<TransactionOutput> getWalletTransactionOutputStream() {
return getTransactions(true).stream()
.flatMap(tx -> tx.getOutputs().stream());
}
private Set<Transaction> getAllBsqTransactionsFromWallet() {
return getAllBsqTxOutputsFromWallet().stream()
.map(TransactionOutput::getParentTransaction)
.collect(Collectors.toSet());
}
public Set<Transaction> getInvalidBsqTransactions() {
Set<Transaction> txsWithOutputsFoundInBsqTxo = getWalletTransactionsWithBsqTxo();
Set<Transaction> txsWithOutputsFoundInBsqTxo = getAllBsqTransactionsFromWallet();
Set<Transaction> walletTxs = getTransactions(true).stream().collect(Collectors.toSet());
checkArgument(walletTxs.size() >= txsWithOutputsFoundInBsqTxo.size(),
"We cannot have more txsWithOutputsFoundInBsqTxo than walletTxs");
@ -323,7 +316,7 @@ public class BsqWalletService extends WalletService {
// non dust BTC output.
// TODO check dust output
CoinSelection coinSelection = bsqCoinSelector.select(fee, getMyBsqUtxosFromWallet());
CoinSelection coinSelection = bsqCoinSelector.select(fee, getMyUnspentBsqTxOutputsFromWallet());
coinSelection.gathered.stream().forEach(tx::addInput);
Coin change = bsqCoinSelector.getChange(fee, coinSelection);
if (change.isPositive())

View File

@ -64,7 +64,7 @@ public abstract class WalletService {
protected final Preferences preferences;
protected final FeeService feeService;
protected final NetworkParameters params;
protected final WalletEventListener walletEventListener = new BisqWalletEventListener();
protected final WalletEventListener walletEventListener = new BisqWalletListener();
protected final CopyOnWriteArraySet<AddressConfidenceListener> addressConfidenceListeners = new CopyOnWriteArraySet<>();
protected final CopyOnWriteArraySet<TxConfidenceListener> txConfidenceListeners = new CopyOnWriteArraySet<>();
protected final CopyOnWriteArraySet<BalanceListener> balanceListeners = new CopyOnWriteArraySet<>();
@ -563,7 +563,7 @@ public abstract class WalletService {
// bisqWalletEventListener
///////////////////////////////////////////////////////////////////////////////////////////
public class BisqWalletEventListener extends AbstractWalletEventListener {
public class BisqWalletListener extends AbstractWalletEventListener {
@Override
public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
notifyBalanceListeners(tx);

View File

@ -22,6 +22,7 @@ import io.bisq.common.app.AppModule;
import io.bisq.core.dao.blockchain.BsqBlockchainManager;
import io.bisq.core.dao.blockchain.BsqBlockchainRpcService;
import io.bisq.core.dao.blockchain.BsqBlockchainService;
import io.bisq.core.dao.blockchain.json.JsonExporter;
import io.bisq.core.dao.compensation.CompensationRequestManager;
import io.bisq.core.dao.vote.VotingDefaultValues;
import io.bisq.core.dao.vote.VotingManager;
@ -44,6 +45,7 @@ public class DaoModule extends AppModule {
bind(DaoManager.class).in(Singleton.class);
bind(BsqBlockchainManager.class).in(Singleton.class);
bind(BsqBlockchainService.class).to(BsqBlockchainRpcService.class).in(Singleton.class);
bind(JsonExporter.class).in(Singleton.class);
bind(DaoPeriodService.class).in(Singleton.class);
bind(VotingService.class).in(Singleton.class);
@ -57,8 +59,6 @@ public class DaoModule extends AppModule {
bindConstant().annotatedWith(named(RpcOptionKeys.RPC_PORT)).to(env.getRequiredProperty(RpcOptionKeys.RPC_PORT));
bindConstant().annotatedWith(named(RpcOptionKeys.RPC_BLOCK_NOTIFICATION_PORT))
.to(env.getRequiredProperty(RpcOptionKeys.RPC_BLOCK_NOTIFICATION_PORT));
bindConstant().annotatedWith(named(RpcOptionKeys.RPC_WALLET_NOTIFICATION_PORT))
.to(env.getRequiredProperty(RpcOptionKeys.RPC_WALLET_NOTIFICATION_PORT));
bindConstant().annotatedWith(named(RpcOptionKeys.DUMP_BLOCKCHAIN_DATA))
.to(env.getRequiredProperty(RpcOptionKeys.DUMP_BLOCKCHAIN_DATA));
}

View File

@ -5,7 +5,6 @@ public class RpcOptionKeys {
public static final String RPC_PASSWORD = "rpcPassword";
public static final String RPC_PORT = "rpcPort";
public static final String RPC_BLOCK_NOTIFICATION_PORT = "rpcBlockNotificationPort";
public static final String RPC_WALLET_NOTIFICATION_PORT = "rpcWalletNotificationPort";
public static final String DUMP_BLOCKCHAIN_DATA = "dumpBlockchainData";
}

View File

@ -17,7 +17,6 @@
package io.bisq.core.dao.blockchain;
import lombok.Getter;
import lombok.Value;
import java.util.ArrayList;
@ -25,11 +24,8 @@ import java.util.List;
@Value
public class BsqBlock {
@Getter
private final int height;
@Getter
private final List<String> txIds;
// private final Map<String, Tx> txByTxIdMap = new HashMap<>();
private final List<Tx> txList = new ArrayList<>();
public BsqBlock(List<String> txIds, int height) {
@ -38,17 +34,12 @@ public class BsqBlock {
}
public void addTx(Tx tx) {
//txByTxIdMap.put(tx.getId(), tx);
txList.add(tx);
}
/* public Tx getTxByTxId(String txId) {
return txByTxIdMap.get(txId);
}*/
@Override
public String toString() {
return "BsqBlock{" +
return "Block{" +
"\nheight=" + height +
",\ntxIds=" + txIds +
",\ntxList=" + txList +

View File

@ -17,36 +17,27 @@
package io.bisq.core.dao.blockchain;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.inject.Inject;
import com.neemre.btcdcli4j.core.domain.PubKeyScript;
import io.bisq.common.UserThread;
import io.bisq.common.handlers.ErrorMessageHandler;
import io.bisq.common.storage.PlainTextWrapper;
import io.bisq.common.storage.Storage;
import io.bisq.common.util.Utilities;
import io.bisq.core.app.BisqEnvironment;
import io.bisq.core.btc.BitcoinNetwork;
import io.bisq.core.dao.RpcOptionKeys;
import io.bisq.core.dao.blockchain.json.ScriptPubKeyForJson;
import io.bisq.core.dao.blockchain.json.SpentInfoForJson;
import io.bisq.core.dao.blockchain.json.TxOutputForJson;
import io.bisq.core.dao.blockchain.json.JsonExporter;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.extern.slf4j.Slf4j;
import javax.inject.Named;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
public class BsqBlockchainManager {
private static final Logger log = LoggerFactory.getLogger(BsqBlockchainManager.class);
///////////////////////////////////////////////////////////////////////////////////////////
// Static fields
///////////////////////////////////////////////////////////////////////////////////////////
//mainnet
private static final String GENESIS_TX_ID = "cabbf6073aea8f22ec678e973ac30c6d8fc89321011da6a017f63e67b9f66667";
@ -65,29 +56,28 @@ public class BsqBlockchainManager {
// new snapshot is block 90. We only persist at the new snapshot, so we always re-parse from latest snapshot after
// a restart.
private static final int SNAPSHOT_TRIGGER = 300000;
private final boolean connectToBtcCore;
public static int getSnapshotTrigger() {
return SNAPSHOT_TRIGGER;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Class fields
///////////////////////////////////////////////////////////////////////////////////////////
private final BsqBlockchainService blockchainService;
private File storageDir;
private final boolean dumpBlockchainData;
private final JsonExporter jsonExporter;
private final BitcoinNetwork bitcoinNetwork;
private final List<BsqUTXOListener> bsqUTXOListeners = new ArrayList<>();
private final List<BsqTxoListener> bsqTxoListeners = new ArrayList<>();
private final List<TxOutputMap.Listener> txOutputMapListeners = new ArrayList<>();
@Getter
private final BsqUTXOMap bsqUTXOMap;
@Getter
private final BsqTXOMap bsqTXOMap;
private final TxOutputMap txOutputMap;
@Getter
private int chainHeadHeight;
@Getter
private boolean isUtxoSyncWithChainHeadHeight;
private final Storage<PlainTextWrapper> jsonStorage;
private boolean parseBlockchainComplete;
private final boolean connectToBtcCore;
///////////////////////////////////////////////////////////////////////////////////////////
@ -97,211 +87,108 @@ public class BsqBlockchainManager {
@Inject
public BsqBlockchainManager(BsqBlockchainService blockchainService,
BisqEnvironment bisqEnvironment,
JsonExporter jsonExporter,
@Named(Storage.DIR_KEY) File storageDir,
Storage<PlainTextWrapper> jsonStorage,
@Named(RpcOptionKeys.RPC_USER) String rpcUser,
@Named(RpcOptionKeys.DUMP_BLOCKCHAIN_DATA) boolean dumpBlockchainData) {
@Named(RpcOptionKeys.RPC_USER) String rpcUser) {
this.blockchainService = blockchainService;
this.storageDir = storageDir;
this.jsonStorage = jsonStorage;
this.dumpBlockchainData = dumpBlockchainData;
this.jsonExporter = jsonExporter;
this.bitcoinNetwork = bisqEnvironment.getBitcoinNetwork();
connectToBtcCore = rpcUser != null && !rpcUser.isEmpty();
bsqUTXOMap = new BsqUTXOMap(storageDir);
bsqTXOMap = new BsqTXOMap(storageDir);
txOutputMap = new TxOutputMap(storageDir);
bsqUTXOMap.addListener(c -> onBsqUTXOChanged());
bsqTXOMap.addListener(c -> onBsqTXOChanged());
bsqTXOMap.addBurnedBSQTxMapListener(c -> onBsqTXOChanged());
if (dumpBlockchainData) {
this.jsonStorage.initWithFileName("txo.json");
/* p2PService.addP2PServiceListener(new BootstrapListener() {
@Override
public void onBootstrapComplete() {
addOfferBookChangedListener(new OfferBookChangedListener() {
@Override
public void onAdded(Offer offer) {
doDumpBlockchainData();
}
@Override
public void onRemoved(Offer offer) {
doDumpBlockchainData();
}
});
UserThread.runAfter(BsqBlockchainManager.this::doDumpBlockchainData, 1);
}
});*/
}
txOutputMap.addListener(bsqTxOutputMap -> onBsqTxoChanged());
}
private void doDumpBlockchainData() {
List<TxOutputForJson> list = bsqTXOMap.getMap().values().stream()
.map(this::getTxOutputForJson)
.collect(Collectors.toList());
list.sort((o1, o2) -> (o1.getSortData().compareTo(o2.getSortData())));
TxOutputForJson[] array = new TxOutputForJson[list.size()];
list.toArray(array);
jsonStorage.queueUpForSave(new PlainTextWrapper(Utilities.objectToJson(array)), 5000);
// keep the individual file storage option as code as we dont know yet what we will use.
/* log.error("txOutputForJson " + txOutputForJson);
File txoDir = new File(Paths.get(storageDir.getAbsolutePath(), "txo").toString());
if (!txoDir.exists())
if (!txoDir.mkdir())
log.warn("make txoDir failed.\ntxoDir=" + txoDir.getAbsolutePath());
File txoFile = new File(Paths.get(txoDir.getAbsolutePath(),
txOutput.getTxId() + ":" + outputIndex + ".json").toString());
// Nr of write requests might be a bit heavy, consider write whole list to one file
FileManager<PlainTextWrapper> fileManager = new FileManager<>(storageDir, txoFile, 1);
fileManager.saveLater(new PlainTextWrapper(Utilities.objectToJson(txOutputForJson)));*/
}
private TxOutputForJson getTxOutputForJson(TxOutput txOutput) {
String txId = txOutput.getTxId();
int outputIndex = txOutput.getIndex();
final long bsqAmount = txOutput.getValue();
final int height = txOutput.getBlockHeight();
final boolean isBsqCoinBase = txOutput.isBsqCoinBase();
final boolean verified = txOutput.isVerified();
final long burnedFee = txOutput.getBurnedFee();
final long btcTxFee = txOutput.getBtcTxFee();
PubKeyScript pubKeyScript = txOutput.getPubKeyScript();
final ScriptPubKeyForJson scriptPubKey = new ScriptPubKeyForJson(pubKeyScript.getAddresses(),
pubKeyScript.getAsm(),
pubKeyScript.getHex(),
pubKeyScript.getReqSigs(),
pubKeyScript.getType().toString());
SpentInfoForJson spentInfoJson = null;
SpendInfo spendInfo = txOutput.getSpendInfo();
if (spendInfo != null)
spentInfoJson = new SpentInfoForJson(spendInfo.getBlockHeight(),
spendInfo.getInputIndex(),
spendInfo.getTxId());
final long time = txOutput.getTime();
final String txVersion = txOutput.getTxVersion();
return new TxOutputForJson(txId,
outputIndex,
bsqAmount,
height,
isBsqCoinBase,
verified,
burnedFee,
btcTxFee,
scriptPubKey,
spentInfoJson,
time,
txVersion
);
}
private void onBsqUTXOChanged() {
bsqUTXOListeners.stream().forEach(e -> e.onBsqUTXOChanged(bsqUTXOMap));
}
private void onBsqTXOChanged() {
bsqTxoListeners.stream().forEach(e -> e.onBsqTxoChanged(bsqTXOMap));
if (dumpBlockchainData)
doDumpBlockchainData();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Public methods
///////////////////////////////////////////////////////////////////////////////////////////
public void onAllServicesInitialized(ErrorMessageHandler errorMessageHandler) {
if (connectToBtcCore)
blockchainService.setup(this::blockchainServiceSetupCompleted, errorMessageHandler);
blockchainService.setup(this::onSetupComplete, errorMessageHandler);
}
public Set<String> getUtxoTxIdSet() {
return bsqUTXOMap.getTxIdSet();
}
public Set<String> getTxoTxIdSet() {
return bsqTXOMap.getTxIdSet();
}
public void addUtxoListener(BsqUTXOListener bsqUTXOListener) {
bsqUTXOListeners.add(bsqUTXOListener);
}
public void removeUtxoListener(BsqUTXOListener bsqUTXOListener) {
bsqUTXOListeners.remove(bsqUTXOListener);
}
public void addTxoListener(BsqTxoListener bsqTxoListener) {
bsqTxoListeners.add(bsqTxoListener);
}
public void removeTxoListener(BsqTxoListener bsqTxoListener) {
bsqTxoListeners.remove(bsqTxoListener);
public void addTxOutputMapListener(TxOutputMap.Listener listener) {
txOutputMapListeners.add(listener);
}
///////////////////////////////////////////////////////////////////////////////////////////
// private methods
// Private methods
///////////////////////////////////////////////////////////////////////////////////////////
private void blockchainServiceSetupCompleted() {
private void onSetupComplete() {
final int genesisBlockHeight = getGenesisBlockHeight();
final String genesisTxId = getGenesisTxId();
int startBlockHeight = Math.max(genesisBlockHeight, bsqUTXOMap.getSnapshotHeight());
log.info("genesisTxId=" + genesisTxId);
log.info("genesisBlockHeight=" + genesisBlockHeight);
log.info("startBlockHeight=" + startBlockHeight);
log.info("bsqUTXOMap.getSnapshotHeight()=" + bsqUTXOMap.getSnapshotHeight());
int startBlockHeight = Math.max(genesisBlockHeight, txOutputMap.getSnapshotHeight());
log.info("parseBlocks with: genesisTxId={}\ngenesisBlockHeight={}\nstartBlockHeight={}\nsnapshotHeight={}",
genesisTxId, genesisBlockHeight, startBlockHeight, txOutputMap.getSnapshotHeight());
if (bsqUTXOMap.getSnapshotHeight() > 0)
onBsqUTXOChanged();
// If we have past data we notify our listeners
if (txOutputMap.getSnapshotHeight() > 0)
onBsqTxoChanged();
ListenableFuture<Integer> future =
blockchainService.executeParseBlockchain(bsqUTXOMap,
bsqTXOMap,
startBlockHeight,
parseBlocks(startBlockHeight,
genesisBlockHeight,
genesisTxId);
}
// TODO handle reorgs
private void parseBlocks(int startBlockHeight, int genesisBlockHeight, String genesisTxId) {
blockchainService.requestChainHeadHeight(chainHeadHeight -> {
if (chainHeadHeight != startBlockHeight) {
blockchainService.parseBlocks(startBlockHeight,
chainHeadHeight,
genesisBlockHeight,
genesisTxId);
genesisTxId,
txOutputMap,
() -> {
// we are done but it might be that new blocks have arrived in the meantime,
// so we try again with startBlockHeight set to current chainHeadHeight
parseBlocks(chainHeadHeight,
genesisBlockHeight,
genesisTxId);
}, throwable -> {
log.error(throwable.toString());
throwable.printStackTrace();
});
} else {
// We dont have received new blocks in the meantime so we are completed and we register our handler
BsqBlockchainManager.this.chainHeadHeight = chainHeadHeight;
parseBlockchainComplete = true;
Futures.addCallback(future, new FutureCallback<Integer>() {
@Override
public void onSuccess(Integer height) {
UserThread.execute(() -> {
chainHeadHeight = height;
isUtxoSyncWithChainHeadHeight = true;
blockchainService.parseBlockchainCompete(btcdBlock -> {
if (btcdBlock != null) {
UserThread.execute(() -> {
try {
final BsqBlock bsqBlock = new BsqBlock(btcdBlock.getTx(), btcdBlock.getHeight());
blockchainService.parseBlock(bsqBlock,
genesisBlockHeight,
genesisTxId,
bsqUTXOMap,
bsqTXOMap);
} catch (BsqBlockchainException e) {
//TODO
e.printStackTrace();
}
// We register our handler for new blocks
blockchainService.addBlockHandler(bsqBlock -> {
blockchainService.parseBlock(bsqBlock,
genesisBlockHeight,
genesisTxId,
txOutputMap,
() -> {
log.debug("new block parsed. bsqBlock={}", bsqBlock);
}, throwable -> {
log.error(throwable.toString());
throwable.printStackTrace();
});
}
});
});
}
@Override
public void onFailure(@NotNull Throwable throwable) {
UserThread.execute(() -> log.error("syncFromGenesis failed" + throwable.toString()));
}
}, throwable -> {
log.error(throwable.toString());
throwable.printStackTrace();
});
}
private void onBsqTxoChanged() {
txOutputMapListeners.stream().forEach(e -> e.onMapChanged(txOutputMap));
jsonExporter.export(txOutputMap);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////////////////////
private String getGenesisTxId() {
return bitcoinNetwork == BitcoinNetwork.REGTEST ? REG_TEST_GENESIS_TX_ID : GENESIS_TX_ID;
}

View File

@ -17,6 +17,7 @@
package io.bisq.core.dao.blockchain;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@ -25,7 +26,6 @@ import com.google.inject.Inject;
import com.neemre.btcdcli4j.core.BitcoindException;
import com.neemre.btcdcli4j.core.CommunicationException;
import com.neemre.btcdcli4j.core.client.BtcdClientImpl;
import com.neemre.btcdcli4j.core.domain.Block;
import com.neemre.btcdcli4j.core.domain.RawTransaction;
import com.neemre.btcdcli4j.daemon.BtcdDaemonImpl;
import com.neemre.btcdcli4j.daemon.event.BlockListener;
@ -51,6 +51,7 @@ import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkNotNull;
// We are in threaded context. We map our results to the client into the UserThread to not extend thread contexts.
public class BsqBlockchainRpcService extends BsqBlockchainService {
private static final Logger log = LoggerFactory.getLogger(BsqBlockchainRpcService.class);
@ -58,13 +59,13 @@ public class BsqBlockchainRpcService extends BsqBlockchainService {
private final String rpcPassword;
private final String rpcPort;
private final String rpcBlockPort;
private final String rpcWalletPort;
private final ListeningExecutorService setupExecutorService = Utilities.getListeningExecutorService("BlockchainRpcService.setup", 1, 1, 5);
private final ListeningExecutorService rpcRequestsExecutor = Utilities.getListeningExecutorService("BlockchainRpcService.requests", 1, 1, 10);
private final ListeningExecutorService setupExecutor = Utilities.getListeningExecutorService("RpcService.setup", 1, 1, 5);
private final ListeningExecutorService parseBlockchainExecutor = Utilities.getListeningExecutorService("RpcService.requests", 1, 1, 10);
private final ListeningExecutorService getChainHeightExecutor = Utilities.getListeningExecutorService("RpcService.requests", 1, 1, 10);
private BtcdClientImpl client;
private BtcdDaemonImpl daemon;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@ -73,23 +74,21 @@ public class BsqBlockchainRpcService extends BsqBlockchainService {
public BsqBlockchainRpcService(@Named(RpcOptionKeys.RPC_USER) String rpcUser,
@Named(RpcOptionKeys.RPC_PASSWORD) String rpcPassword,
@Named(RpcOptionKeys.RPC_PORT) String rpcPort,
@Named(RpcOptionKeys.RPC_BLOCK_NOTIFICATION_PORT) String rpcBlockPort,
@Named(RpcOptionKeys.RPC_WALLET_NOTIFICATION_PORT) String rpcWalletPort) {
@Named(RpcOptionKeys.RPC_BLOCK_NOTIFICATION_PORT) String rpcBlockPort) {
this.rpcUser = rpcUser;
this.rpcPassword = rpcPassword;
this.rpcPort = rpcPort;
this.rpcBlockPort = rpcBlockPort;
this.rpcWalletPort = rpcWalletPort;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Protected methods
// Non blocking methods
///////////////////////////////////////////////////////////////////////////////////////////
@Override
void setup(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
ListenableFuture<BtcdClientImpl> future = setupExecutorService.submit(() -> {
ListenableFuture<BtcdClientImpl> future = setupExecutor.submit(() -> {
long startTs = System.currentTimeMillis();
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
CloseableHttpClient httpProvider = HttpClients.custom().setConnectionManager(cm).build();
@ -103,7 +102,6 @@ public class BsqBlockchainRpcService extends BsqBlockchainService {
nodeConfig.setProperty("node.bitcoind.rpc.password", rpcPassword);
nodeConfig.setProperty("node.bitcoind.rpc.port", rpcPort);
nodeConfig.setProperty("node.bitcoind.notification.block.port", rpcBlockPort);
nodeConfig.setProperty("node.bitcoind.notification.wallet.port", rpcWalletPort);
BtcdClientImpl client = new BtcdClientImpl(httpProvider, nodeConfig);
daemon = new BtcdDaemonImpl(client);
log.info("Setup took {} ms", System.currentTimeMillis() - startTs);
@ -141,64 +139,119 @@ public class BsqBlockchainRpcService extends BsqBlockchainService {
}
@Override
protected ListenableFuture<Integer> executeParseBlockchain(BsqUTXOMap bsqUTXOMap,
BsqTXOMap bsqTXOMap,
int startBlockHeight,
int genesisBlockHeight,
String genesisTxId) {
return rpcRequestsExecutor.submit(() -> {
long startTs = System.currentTimeMillis();
int chainHeadHeight = requestChainHeadHeight();
parseBlockchain(bsqUTXOMap,
bsqTXOMap,
chainHeadHeight,
startBlockHeight,
genesisBlockHeight,
genesisTxId);
log.info("syncFromGenesis took {} ms", System.currentTimeMillis() - startTs);
return chainHeadHeight;
void requestChainHeadHeight(Consumer<Integer> resultHandler, Consumer<Throwable> errorHandler) {
ListenableFuture<Integer> future = getChainHeightExecutor.submit(client::getBlockCount);
Futures.addCallback(future, new FutureCallback<Integer>() {
public void onSuccess(Integer chainHeadHeight) {
UserThread.execute(() -> resultHandler.accept(chainHeadHeight));
}
public void onFailure(@NotNull Throwable throwable) {
UserThread.execute(() -> errorHandler.accept(throwable));
}
});
}
@Override
protected void parseBlockchainCompete(Consumer<Block> onNewBlockHandler) {
daemon.addBlockListener(new BlockListener() {
void parseBlocks(int startBlockHeight,
int chainHeadHeight,
int genesisBlockHeight,
String genesisTxId,
TxOutputMap txOutputMap,
ResultHandler resultHandler,
Consumer<Throwable> errorHandler) {
ListenableFuture<Void> future = parseBlockchainExecutor.submit(() -> {
long startTs = System.currentTimeMillis();
BsqParser bsqParser = new BsqParser(this);
bsqParser.parseBlocks(startBlockHeight,
chainHeadHeight,
genesisBlockHeight,
genesisTxId, txOutputMap);
log.info("parseBlockchain took {} ms", System.currentTimeMillis() - startTs);
return null;
});
Futures.addCallback(future, new FutureCallback<Void>() {
@Override
public void blockDetected(Block block) {
log.info("blockDetected " + block.getHash());
if (onNewBlockHandler != null)
onNewBlockHandler.accept(block);
public void onSuccess(Void ignore) {
UserThread.execute(resultHandler::handleResult);
}
@Override
public void onFailure(@NotNull Throwable throwable) {
errorHandler.accept(throwable);
}
});
/* daemon.addWalletListener(new WalletListener() {
@Override
public void walletChanged(Transaction transaction) {
log.info("walletChanged " + transaction.getTxId());
*//* try {
// parseTransaction(transaction.getTxId(), GENESIS_TX_ID, client.getBlockCount(), tempUtxoByTxIdMap, bsqUTXOMap);
printUtxoMap(bsqUTXOMap);
} catch (BitcoindException | CommunicationException | BsqBlockchainException e) {
//TODO
e.printStackTrace();
}*//*
}
});*/
}
@Override
void parseBlock(BsqBlock block,
int genesisBlockHeight,
String genesisTxId,
TxOutputMap txOutputMap,
ResultHandler resultHandler,
Consumer<Throwable> errorHandler) {
ListenableFuture<Void> future = parseBlockchainExecutor.submit(() -> {
BsqParser bsqParser = new BsqParser(this);
bsqParser.parseBlock(block,
genesisBlockHeight,
genesisTxId,
txOutputMap);
return null;
});
Futures.addCallback(future, new FutureCallback<Void>() {
@Override
public void onSuccess(Void ignore) {
UserThread.execute(resultHandler::handleResult);
}
@Override
public void onFailure(@NotNull Throwable throwable) {
errorHandler.accept(throwable);
}
});
}
@Override
void addBlockHandler(Consumer<BsqBlock> blockHandler) {
daemon.addBlockListener(new BlockListener() {
@Override
public void blockDetected(com.neemre.btcdcli4j.core.domain.Block btcdBlock) {
if (btcdBlock != null) {
UserThread.execute(() -> {
log.info("new block detected " + btcdBlock.getHash());
blockHandler.accept(new BsqBlock(btcdBlock.getTx(), btcdBlock.getHeight()));
});
}
}
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// Blocking methods
///////////////////////////////////////////////////////////////////////////////////////////
@VisibleForTesting
@Override
int requestChainHeadHeight() throws BitcoindException, CommunicationException {
return client.getBlockCount();
}
@VisibleForTesting
@Override
Block requestBlock(int blockHeight) throws BitcoindException, CommunicationException {
com.neemre.btcdcli4j.core.domain.Block requestBlock(int blockHeight) throws BitcoindException, CommunicationException {
return client.getBlock(client.getBlockHash(blockHeight));
}
@VisibleForTesting
@Override
Tx requestTransaction(String txId, int blockHeight) throws BsqBlockchainException {
try {
RawTransaction rawTransaction = getRawTransaction(txId);
RawTransaction rawTransaction = requestRawTransaction(txId);
// rawTransaction.getTime() is in seconds but we keep it in ms internally
final long time = rawTransaction.getTime() * 1000;
final List<TxInput> txInputs = rawTransaction.getVIn()
.stream()
@ -209,18 +262,13 @@ public class BsqBlockchainRpcService extends BsqBlockchainService {
final List<TxOutput> txOutputs = rawTransaction.getVOut()
.stream()
.filter(e -> e != null && e.getN() != null && e.getValue() != null && e.getScriptPubKey() != null)
.map(rawOutput -> {
return new TxOutput(rawOutput.getN(),
rawOutput.getValue().movePointRight(8).longValue(),
rawTransaction.getTxId(),
rawOutput.getScriptPubKey(),
blockHeight,
time);
})
.map(rawOutput -> new TxOutput(rawOutput.getN(),
rawOutput.getValue().movePointRight(8).longValue(),
rawTransaction.getTxId(),
rawOutput.getScriptPubKey(),
blockHeight,
time))
.collect(Collectors.toList());
// rawTransaction.getTime() is in seconds but we keep it in ms internally
return new Tx(txId,
txInputs,
txOutputs,
@ -230,7 +278,10 @@ public class BsqBlockchainRpcService extends BsqBlockchainService {
}
}
protected RawTransaction getRawTransaction(String txId) throws BitcoindException, CommunicationException {
@VisibleForTesting
@Override
RawTransaction requestRawTransaction(String txId) throws BitcoindException, CommunicationException {
return (RawTransaction) client.getRawTransaction(txId, 1);
}
}

View File

@ -18,31 +18,18 @@
package io.bisq.core.dao.blockchain;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.inject.Inject;
import com.neemre.btcdcli4j.core.BitcoindException;
import com.neemre.btcdcli4j.core.CommunicationException;
import com.neemre.btcdcli4j.core.domain.Block;
import io.bisq.common.app.DevEnv;
import com.neemre.btcdcli4j.core.domain.RawTransaction;
import io.bisq.common.handlers.ErrorMessageHandler;
import io.bisq.common.handlers.ResultHandler;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkArgument;
@Slf4j
abstract public class BsqBlockchainService {
private int snapshotHeight;
// todo just temp
// Map<Integer, String> recursionMap = new HashMap<>();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -54,295 +41,44 @@ abstract public class BsqBlockchainService {
///////////////////////////////////////////////////////////////////////////////////////////
// Protected methods
// Non blocking methods
///////////////////////////////////////////////////////////////////////////////////////////
abstract void setup(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler);
abstract ListenableFuture<Integer> executeParseBlockchain(BsqUTXOMap bsqUTXOMap,
BsqTXOMap bsqTXOMap,
int startBlockHeight,
int genesisBlockHeight,
String genesisTxId);
abstract void requestChainHeadHeight(Consumer<Integer> resultHandler, Consumer<Throwable> errorHandler);
abstract void parseBlockchainCompete(Consumer<Block> onNewBlockHandler);
abstract void parseBlocks(int startBlockHeight,
int chainHeadHeight,
int genesisBlockHeight,
String genesisTxId,
TxOutputMap txOutputMap,
ResultHandler resultHandler,
Consumer<Throwable> errorHandler);
abstract void addBlockHandler(Consumer<BsqBlock> onNewBlockHandler);
abstract void parseBlock(BsqBlock block,
int genesisBlockHeight,
String genesisTxId,
TxOutputMap txOutputMap,
ResultHandler resultHandler,
Consumer<Throwable> errorHandler);
///////////////////////////////////////////////////////////////////////////////////////////
// Blocking methods
///////////////////////////////////////////////////////////////////////////////////////////
@VisibleForTesting
abstract int requestChainHeadHeight() throws BitcoindException, CommunicationException;
abstract Block requestBlock(int i) throws BitcoindException, CommunicationException;
@VisibleForTesting
abstract com.neemre.btcdcli4j.core.domain.Block requestBlock(int i) throws BitcoindException, CommunicationException;
@VisibleForTesting
abstract Tx requestTransaction(String txId, int blockHeight) throws BsqBlockchainException;
@VisibleForTesting
void parseBlockchain(BsqUTXOMap bsqUTXOMap,
BsqTXOMap bsqTXOMap,
int chainHeadHeight,
int startBlockHeight,
int genesisBlockHeight,
String genesisTxId) throws BsqBlockchainException {
try {
log.info("chainHeadHeight=" + chainHeadHeight);
long startTotalTs = System.currentTimeMillis();
for (int height = startBlockHeight; height <= chainHeadHeight; height++) {
long startBlockTs = System.currentTimeMillis();
Block btcdBlock = requestBlock(height);
log.debug("Current block height=" + height);
// 1 block has about 3 MB
final BsqBlock bsqBlock = new BsqBlock(btcdBlock.getTx(), btcdBlock.getHeight());
// String oldBsqUTXOMap = bsqUTXOMap.toString();
parseBlock(bsqBlock,
genesisBlockHeight,
genesisTxId,
bsqUTXOMap,
bsqTXOMap);
// String newBsqUTXOMap = bsqUTXOMap.toString();
/* StringBuilder sb = new StringBuilder("recursionMap:\n");
List<String> list = new ArrayList<>();
//recursionMap.entrySet().stream().forEach(e -> sb.append(e.getKey()).append(": ").append(e.getValue()).append("\n"));
recursionMap.entrySet().stream().forEach(e -> list.add("\nBlock height / Tx graph depth / Nr. of Txs: " + e.getKey()
+ " / " + e.getValue()));
Collections.sort(list);
list.stream().forEach(e -> sb.append(e).append("\n"));
log.warn(list.toString().replace(",", "").replace("[", "").replace("]", ""));*/
/* log.info("Parsing for block {} took {} ms. Total: {} ms for {} blocks",
height,
(System.currentTimeMillis() - startBlockTs),
(System.currentTimeMillis() - startTotalTs),
(height - startBlockHeight + 1));
Profiler.printSystemLoad(log);*/
}
log.info("Parsing for all blocks since genesis took {} ms", System.currentTimeMillis() - startTotalTs);
} catch (Throwable t) {
log.error(t.toString());
t.printStackTrace();
throw new BsqBlockchainException(t);
}
}
void parseBlock(BsqBlock block,
int genesisBlockHeight,
String genesisTxId,
BsqUTXOMap bsqUTXOMap,
BsqTXOMap bsqTXOMap)
throws BsqBlockchainException {
int blockHeight = block.getHeight();
log.debug("Parse block at height={} ", blockHeight);
// We add all transactions to the block
// TODO here we hve the performance bottleneck. takes about 4 sec.
// check if there is more efficient rpc calls for tx ranges or all txs in a block with btcd 14
List<String> txIds = block.getTxIds();
Tx genesisTx = null;
for (String txId : txIds) {
final Tx tx = requestTransaction(txId, blockHeight);
block.addTx(tx);
if (txId.equals(genesisTxId))
genesisTx = tx;
}
// First we check for the genesis tx
// All outputs of genesis are valid BSQ UTXOs
if (genesisTx != null) {
checkArgument(blockHeight == genesisBlockHeight,
"If we have a matching genesis tx the block height must mathc as well");
parseGenesisTx(genesisTx, blockHeight, bsqUTXOMap, bsqTXOMap);
}
//txSize = block.getTxList().size();
// Worst case is that all txs in a block are depending on another, so only once get resolved at each iteration.
// Worst case is that all txs in a block are depending on another, so only one get resolved at each iteration.
// Min tx size is 189 bytes (normally about 240 bytes), 1 MB can contain max. about 5300 txs (usually 2000).
// Realistically we don't expect more then a few recursive calls.
// There are some blocks with testing such dependency chains like block 130768 where at each iteration only
// one get resolved.
updateBsqUtxoMapFromBlock(block.getTxList(), bsqUTXOMap, bsqTXOMap, blockHeight, 0, 5300);
int trigger = BsqBlockchainManager.getSnapshotTrigger();
if (blockHeight % trigger == 0 && blockHeight > snapshotHeight - trigger) {
snapshotHeight = blockHeight - trigger;
log.info("We reached a new snapshot trigger at height {}. New snapshotHeight is {}",
blockHeight, snapshotHeight);
bsqUTXOMap.setSnapshotHeight(snapshotHeight);
bsqUTXOMap.persist();
bsqTXOMap.setSnapshotHeight(snapshotHeight);
bsqTXOMap.persist();
}
}
//private int txSize;
// Recursive method
// Performance-wise the recursion does not hurt (e.g. 5-20 ms).
void updateBsqUtxoMapFromBlock(List<Tx> transactions,
BsqUTXOMap bsqUTXOMap,
BsqTXOMap bsqTXOMap,
int blockHeight,
int recursionCounter,
int maxRecursions) {
//recursionMap.put(blockHeight, recursionCounter + " / " + txSize);
// The set of txIds of txs which are used for inputs of another tx in same block
Set<String> intraBlockSpendingTxIdSet = getIntraBlockSpendingTxIdSet(transactions);
List<Tx> txsWithoutInputsFromSameBlock = new ArrayList<>();
List<Tx> txsWithInputsFromSameBlock = new ArrayList<>();
// First we find the txs which have no intra-block inputs
outerLoop:
for (Tx tx : transactions) {
for (TxInput input : tx.getInputs()) {
if (intraBlockSpendingTxIdSet.contains(input.getSpendingTxId())) {
// We have an input from one of the intra-block-transactions, so we cannot process that tx now.
// We add the tx for later parsing to the txsWithInputsFromSameBlock and move to the next
// outerLoop iteration .
txsWithInputsFromSameBlock.add(tx);
continue outerLoop;
}
}
// If we have not found any tx input pointing to anther tx in the same block we add it to our
// txsWithoutInputsFromSameBlock for first pass of BSQ validation
txsWithoutInputsFromSameBlock.add(tx);
}
// Usual values is up to 25
// There are some blocks where it seems devs have tested graphs of many depending txs, but even
// those dont exceed 200 recursions and are mostly old blocks from 2012 when fees have been low ;-).
// TODO check strategy btc core uses (sorting the dependency graph would be an optimisation)
// Seems btc core delivers tx list sorted by dependency graph. -> TODO verify and test
if (recursionCounter > 100) {
log.warn("Unusual high recursive calls at resolveConnectedTxs. recursionCounter=" + recursionCounter);
log.warn("blockHeight=" + blockHeight);
log.warn("txsWithoutInputsFromSameBlock.size " + txsWithoutInputsFromSameBlock.size());
log.warn("txsWithInputsFromSameBlock.size " + txsWithInputsFromSameBlock.size());
// if (txsWithoutInputsFromSameBlock.size() == 1)
// log.warn("txsWithInputsFromSameBlock " + txsWithInputsFromSameBlock.stream().map(e->e.getId()).collect(Collectors.toList()));
}
// we check if we have any valid BSQ utxo from that tx set
if (!txsWithoutInputsFromSameBlock.isEmpty()) {
for (Tx tx : txsWithoutInputsFromSameBlock) {
updateBsqUtxoMapFromTx(tx, blockHeight, bsqUTXOMap, bsqTXOMap);
}
}
// we check if we have any valid BSQ utxo from that tx set
// We might have InputsFromSameBlock whcih are BTC only but not BSQ, so we cannot
// optimize here and need to iterate further.
// TODO recursion risk?
if (!txsWithInputsFromSameBlock.isEmpty()) {
if (recursionCounter < maxRecursions) {
updateBsqUtxoMapFromBlock(txsWithInputsFromSameBlock, bsqUTXOMap, bsqTXOMap, blockHeight,
++recursionCounter, maxRecursions);
} else {
final String msg = "We exceeded our max. recursions for resolveConnectedTxs.\n" +
"txsWithInputsFromSameBlock=" + txsWithInputsFromSameBlock.toString() + "\n" +
"txsWithoutInputsFromSameBlock=" + txsWithoutInputsFromSameBlock;
log.warn(msg);
if (DevEnv.DEV_MODE)
throw new RuntimeException(msg);
}
} else {
log.debug("We have no more txsWithInputsFromSameBlock.");
}
}
private Set<String> getIntraBlockSpendingTxIdSet(List<Tx> transactions) {
Set<String> txIdSet = transactions.stream().map(Tx::getId).collect(Collectors.toSet());
Set<String> intraBlockSpendingTxIdSet = new HashSet<>();
transactions.stream()
.forEach(tx -> tx.getInputs().stream()
.filter(input -> txIdSet.contains(input.getSpendingTxId()))
.forEach(input -> intraBlockSpendingTxIdSet.add(input.getSpendingTxId())));
return intraBlockSpendingTxIdSet;
}
private boolean updateBsqUtxoMapFromTx(Tx tx,
int blockHeight,
BsqUTXOMap bsqUTXOMap,
BsqTXOMap bsqTXOMap) {
List<TxOutput> outputs = tx.getOutputs();
boolean utxoChanged = false;
long availableValue = 0;
long totalAvailableBSQInputs = 0;
long totalSpendBSQOutputs = 0;
final String txId = tx.getId();
for (int inputIndex = 0; inputIndex < tx.getInputs().size(); inputIndex++) {
TxInput input = tx.getInputs().get(inputIndex);
String spendingTxId = input.getSpendingTxId();
final int spendingTxOutputIndex = input.getSpendingTxOutputIndex();
if (bsqTXOMap.containsTuple(spendingTxId, spendingTxOutputIndex)) {
TxOutput txOutputFromSpendingTx = bsqTXOMap.getByTuple(spendingTxId, spendingTxOutputIndex);
txOutputFromSpendingTx.setSpendInfo(new SpendInfo(blockHeight, txId, inputIndex));
}
if (bsqUTXOMap.containsTuple(spendingTxId, spendingTxOutputIndex)) {
BsqUTXO bsqUTXO = bsqUTXOMap.getByTuple(spendingTxId, spendingTxOutputIndex);
availableValue = availableValue + bsqUTXO.getValue();
totalAvailableBSQInputs += bsqUTXO.getValue();
bsqUTXOMap.removeByTuple(spendingTxId, spendingTxOutputIndex);
utxoChanged = true;
if (bsqUTXOMap.isEmpty())
break;
}
}
// If we have an input spending tokens we iterate the outputs
if (availableValue > 0) {
// We use order of output index. An output is a BSQ utxo as long there is enough input value
for (TxOutput txOutput : outputs) {
availableValue = availableValue - txOutput.getValue();
if (availableValue >= 0) {
// We are spending available tokens
txOutput.setVerified(true);
bsqUTXOMap.add(new BsqUTXO(blockHeight, txOutput, false));
bsqTXOMap.add(txOutput);
totalSpendBSQOutputs += txOutput.getValue();
if (availableValue == 0) {
log.debug("We don't have anymore BSQ to spend");
break;
}
} else {
log.debug("We tried to spend more BSQ as we have in our inputs");
break;
}
}
}
final long burnedAmount = totalAvailableBSQInputs - totalSpendBSQOutputs;
if (burnedAmount > 0) {
log.debug("BSQ have been left which was not spent. Burned BSQ amount={}, tx={}",
availableValue,
tx.toString());
tx.getOutputs().stream().forEach(e -> e.setBurnedFee(burnedAmount));
bsqTXOMap.addBurnedBSQTx(tx);
}
return utxoChanged;
}
@VisibleForTesting
void parseGenesisTx(Tx tx,
int blockHeight,
BsqUTXOMap bsqUTXOMap,
BsqTXOMap bsqTXOMap) {
List<TxOutput> outputs = tx.getOutputs();
//TODO use BsqTXO not BsqUTXO as we dont know if they are unspent
// Genesis tx uses all outputs as BSQ outputs
for (TxOutput txOutput : outputs) {
txOutput.setVerified(true);
txOutput.setBsqCoinBase(true);
bsqUTXOMap.add(new BsqUTXO(blockHeight, txOutput, true));
bsqTXOMap.add(txOutput);
}
checkArgument(!bsqUTXOMap.isEmpty(), "Genesis tx need to have BSQ utxo when parsing genesis block");
}
abstract RawTransaction requestRawTransaction(String txId) throws BitcoindException, CommunicationException;
}

View File

@ -0,0 +1,283 @@
/*
* This file is part of bisq.
*
* bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bisq.core.dao.blockchain;
import com.google.common.annotations.VisibleForTesting;
import io.bisq.common.app.DevEnv;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.concurrent.Immutable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkArgument;
// We are in threaded context. Don't mix up with UserThread.
@Slf4j
@Immutable
public class BsqParser {
// todo just temp
// Map<Integer, String> recursionMap = new HashMap<>();
private final BsqBlockchainService bsqBlockchainService;
public BsqParser(BsqBlockchainService bsqBlockchainService) {
this.bsqBlockchainService = bsqBlockchainService;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Parsing
///////////////////////////////////////////////////////////////////////////////////////////
@VisibleForTesting
void parseBlocks(int startBlockHeight,
int chainHeadHeight,
int genesisBlockHeight,
String genesisTxId,
TxOutputMap txOutputMap) throws BsqBlockchainException {
try {
log.info("chainHeadHeight=" + chainHeadHeight);
long startTotalTs = System.currentTimeMillis();
for (int height = startBlockHeight; height <= chainHeadHeight; height++) {
long startBlockTs = System.currentTimeMillis();
com.neemre.btcdcli4j.core.domain.Block btcdBlock = bsqBlockchainService.requestBlock(height);
log.debug("Current block height=" + height);
// 1 block has about 3 MB, but we keep it only in memory as long as needed
final BsqBlock bsqBlock = new BsqBlock(btcdBlock.getTx(), btcdBlock.getHeight());
parseBlock(bsqBlock,
genesisBlockHeight,
genesisTxId,
txOutputMap);
/* StringBuilder sb = new StringBuilder("recursionMap:\n");
List<String> list = new ArrayList<>();
//recursionMap.entrySet().stream().forEach(e -> sb.append(e.getKey()).append(": ").append(e.getValue()).append("\n"));
recursionMap.entrySet().stream().forEach(e -> list.add("\nBlock height / Tx graph depth / Nr. of Txs: " + e.getKey()
+ " / " + e.getValue()));
Collections.sort(list);
list.stream().forEach(e -> sb.append(e).append("\n"));
log.warn(list.toString().replace(",", "").replace("[", "").replace("]", ""));*/
/* log.info("Parsing for block {} took {} ms. Total: {} ms for {} blocks",
height,
(System.currentTimeMillis() - startBlockTs),
(System.currentTimeMillis() - startTotalTs),
(height - startBlockHeight + 1));
Profiler.printSystemLoad(log);*/
}
log.info("Parsing for all blocks since genesis took {} ms", System.currentTimeMillis() - startTotalTs);
} catch (Throwable t) {
log.error(t.toString());
t.printStackTrace();
throw new BsqBlockchainException(t);
}
}
void parseBlock(BsqBlock block,
int genesisBlockHeight,
String genesisTxId,
TxOutputMap txOutputMap)
throws BsqBlockchainException {
int blockHeight = block.getHeight();
log.debug("Parse block at height={} ", blockHeight);
// We add all transactions to the block
List<String> txIds = block.getTxIds();
Tx genesisTx = null;
for (String txId : txIds) {
final Tx tx = bsqBlockchainService.requestTransaction(txId, blockHeight);
block.addTx(tx);
if (txId.equals(genesisTxId))
genesisTx = tx;
}
if (genesisTx != null) {
checkArgument(blockHeight == genesisBlockHeight,
"If we have a matching genesis tx the block height must match as well");
parseGenesisTx(genesisTx, txOutputMap);
}
//txSize = block.getTxList().size();
// Worst case is that all txs in a block are depending on another, so only one get resolved at each iteration.
// Min tx size is 189 bytes (normally about 240 bytes), 1 MB can contain max. about 5300 txs (usually 2000).
// Realistically we don't expect more then a few recursive calls.
// There are some blocks with testing such dependency chains like block 130768 where at each iteration only
// one get resolved.
// Lately there is a patter with 24 iterations observed
parseTransactions(block.getTxList(), txOutputMap, blockHeight, 0, 5300);
// We persist only at certain snapshots. They have a safety distance from head so re-orgs should not cause
// issues in our persisted data.
int trigger = BsqBlockchainManager.getSnapshotTrigger();
int snapshotHeight = txOutputMap.getSnapshotHeight();
if (blockHeight % trigger == 0 && blockHeight > snapshotHeight - trigger) {
snapshotHeight = blockHeight - trigger;
log.info("We reached a new snapshot trigger at height {}. New snapshotHeight is {}",
blockHeight, snapshotHeight);
txOutputMap.setSnapshotHeight(snapshotHeight);
txOutputMap.persist();
}
}
@VisibleForTesting
void parseGenesisTx(Tx tx,
TxOutputMap txOutputMap) {
// Genesis tx uses all outputs as BSQ outputs
tx.getOutputs().stream().forEach(txOutput -> {
txOutput.setVerified(true);
txOutput.setBsqCoinBase(true);
txOutputMap.put(txOutput);
});
}
// Recursive method
// Performance-wise the recursion does not hurt (e.g. 5-20 ms).
// The RPC requestTransaction is the slow call.
void parseTransactions(List<Tx> transactions,
TxOutputMap txOutputMap,
int blockHeight,
int recursionCounter,
int maxRecursions) {
//recursionMap.put(blockHeight, recursionCounter + " / " + txSize);
// The set of txIds of txs which are used for inputs of another tx in same block
Set<String> intraBlockSpendingTxIdSet = getIntraBlockSpendingTxIdSet(transactions);
List<Tx> txsWithoutInputsFromSameBlock = new ArrayList<>();
List<Tx> txsWithInputsFromSameBlock = new ArrayList<>();
// First we find the txs which have no intra-block inputs
outerLoop:
for (Tx tx : transactions) {
for (TxInput input : tx.getInputs()) {
if (intraBlockSpendingTxIdSet.contains(input.getSpendingTxId())) {
// We have an input from one of the intra-block-transactions, so we cannot process that tx now.
// We add the tx for later parsing to the txsWithInputsFromSameBlock and move to the next tx.
txsWithInputsFromSameBlock.add(tx);
continue outerLoop;
}
}
// If we have not found any tx input pointing to anther tx in the same block we add it to our
// txsWithoutInputsFromSameBlock.
txsWithoutInputsFromSameBlock.add(tx);
}
checkArgument(txsWithInputsFromSameBlock.size() + txsWithoutInputsFromSameBlock.size() == transactions.size(),
"txsWithInputsFromSameBlock.size + txsWithoutInputsFromSameBlock.size != transactions.size");
// Usual values is up to 25
// There are some blocks where it seems devs have tested graphs of many depending txs, but even
// those dont exceed 200 recursions and are mostly old blocks from 2012 when fees have been low ;-).
// TODO check strategy btc core uses (sorting the dependency graph would be an optimisation)
// Seems btc core delivers tx list sorted by dependency graph. -> TODO verify and test
if (recursionCounter > 100) {
log.warn("Unusual high recursive calls at resolveConnectedTxs. recursionCounter=" + recursionCounter);
log.warn("blockHeight=" + blockHeight);
log.warn("txsWithoutInputsFromSameBlock.size " + txsWithoutInputsFromSameBlock.size());
log.warn("txsWithInputsFromSameBlock.size " + txsWithInputsFromSameBlock.size());
// log.warn("txsWithInputsFromSameBlock " + txsWithInputsFromSameBlock.stream().map(e->e.getId()).collect(Collectors.toList()));
}
// we check if we have any valid BSQ from that tx set
if (!txsWithoutInputsFromSameBlock.isEmpty()) {
for (Tx tx : txsWithoutInputsFromSameBlock) {
parseTx(tx, blockHeight, txOutputMap);
}
log.debug("Parsing of all txsWithoutInputsFromSameBlock is done.");
}
// we check if we have any valid BSQ utxo from that tx set
// We might have InputsFromSameBlock which are BTC only but not BSQ, so we cannot
// optimize here and need to iterate further.
if (!txsWithInputsFromSameBlock.isEmpty()) {
if (recursionCounter < maxRecursions) {
parseTransactions(txsWithInputsFromSameBlock, txOutputMap, blockHeight,
++recursionCounter, maxRecursions);
} else {
final String msg = "We exceeded our max. recursions for resolveConnectedTxs.\n" +
"txsWithInputsFromSameBlock=" + txsWithInputsFromSameBlock.toString() + "\n" +
"txsWithoutInputsFromSameBlock=" + txsWithoutInputsFromSameBlock;
log.warn(msg);
if (DevEnv.DEV_MODE)
throw new RuntimeException(msg);
}
} else {
log.debug("We have no more txsWithInputsFromSameBlock.");
}
}
private void parseTx(Tx tx,
int blockHeight,
TxOutputMap txOutputMap) {
List<TxOutput> outputs = tx.getOutputs();
long availableValue = 0;
final String txId = tx.getId();
for (int inputIndex = 0; inputIndex < tx.getInputs().size(); inputIndex++) {
TxInput input = tx.getInputs().get(inputIndex);
final TxOutput txOutputFromSpendingTx = txOutputMap.get(input.getSpendingTxId(), input.getSpendingTxOutputIndex());
if (txOutputFromSpendingTx != null &&
txOutputFromSpendingTx.isVerified() &&
txOutputFromSpendingTx.isUnSpend()) {
txOutputFromSpendingTx.setSpendInfo(new SpendInfo(blockHeight, txId, inputIndex));
availableValue = availableValue + txOutputFromSpendingTx.getValue();
}
}
// If we have an input spending tokens we iterate the outputs
if (availableValue > 0) {
// We use order of output index. An output is a BSQ utxo as long there is enough input value
for (TxOutput txOutput : outputs) {
final long txOutputValue = txOutput.getValue();
if (availableValue >= txOutputValue) {
// We are spending available tokens
txOutput.setVerified(true);
txOutputMap.put(txOutput);
availableValue -= txOutputValue;
if (availableValue == 0) {
log.debug("We don't have anymore BSQ to spend");
break;
}
} else {
break;
}
}
}
if (availableValue > 0) {
log.debug("BSQ have been left which was not spent. Burned BSQ amount={}, tx={}",
availableValue,
tx.toString());
final long finalAvailableValue = availableValue;
tx.getOutputs().stream().forEach(e -> e.setBurnedFee(finalAvailableValue));
}
}
private Set<String> getIntraBlockSpendingTxIdSet(List<Tx> transactions) {
Set<String> txIdSet = transactions.stream().map(Tx::getId).collect(Collectors.toSet());
Set<String> intraBlockSpendingTxIdSet = new HashSet<>();
transactions.stream()
.forEach(tx -> tx.getInputs().stream()
.filter(input -> txIdSet.contains(input.getSpendingTxId()))
.forEach(input -> intraBlockSpendingTxIdSet.add(input.getSpendingTxId())));
return intraBlockSpendingTxIdSet;
}
}

View File

@ -1,133 +0,0 @@
/*
* This file is part of Bitsquare.
*
* Bitsquare is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bisq.core.dao.blockchain;
import io.bisq.common.storage.Storage;
import javafx.collections.FXCollections;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableMap;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.*;
// Map of any ever existing TxOutput which was a valid BSQ
@Slf4j
public class BsqTXOMap implements Serializable {
// We don't use a Lombok delegate here as we want control the access to our map
@Getter
private HashMap<TxIdIndexTuple, TxOutput> map = new HashMap<>();
@Getter
private HashSet<String> txIdSet = new HashSet<>();
@Getter
private HashMap<String, Tx> burnedBSQTxMap = new HashMap<>();
@Getter
@Setter
private int snapshotHeight;
private transient ObservableMap<TxIdIndexTuple, TxOutput> observableMap;
private transient ObservableMap<String, Tx> observableBurnedBSQTxMap;
private transient final Storage<BsqTXOMap> storage;
public BsqTXOMap(File storageDir) {
storage = new Storage<>(storageDir);
BsqTXOMap persisted = storage.initAndGetPersisted(this, "BsqTXOMap");
if (persisted != null) {
map.putAll(persisted.getMap());
snapshotHeight = persisted.getSnapshotHeight();
txIdSet = persisted.getTxIdSet();
}
observableMap = FXCollections.observableHashMap();
observableMap.putAll(map);
observableBurnedBSQTxMap = FXCollections.observableHashMap();
observableBurnedBSQTxMap.putAll(burnedBSQTxMap);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
try {
in.defaultReadObject();
observableMap = FXCollections.observableHashMap();
observableMap.putAll(map);
observableBurnedBSQTxMap = FXCollections.observableHashMap();
observableBurnedBSQTxMap.putAll(burnedBSQTxMap);
} catch (Throwable t) {
log.warn("Cannot be deserialized." + t.getMessage());
}
}
public Object add(TxOutput txOutput) {
txIdSet.add(txOutput.getTxId());
final TxOutput result = map.put(new TxIdIndexTuple(txOutput.getTxId(), txOutput.getIndex()), txOutput);
observableMap.put(new TxIdIndexTuple(txOutput.getTxId(), txOutput.getIndex()), txOutput);
return result;
}
public Object addBurnedBSQTx(Tx tx) {
Tx result = burnedBSQTxMap.put(tx.getId(), tx);
observableBurnedBSQTxMap.put(tx.getId(), tx);
return result;
}
public void persist() {
storage.queueUpForSave();
}
public boolean containsTuple(String txId, int index) {
return map.containsKey(new TxIdIndexTuple(txId, index));
}
public TxOutput getByTuple(String txId, int index) {
return map.get(new TxIdIndexTuple(txId, index));
}
public void addListener(MapChangeListener<TxIdIndexTuple, TxOutput> listener) {
observableMap.addListener(listener);
}
public void addBurnedBSQTxMapListener(MapChangeListener<String, Tx> listener) {
observableBurnedBSQTxMap.addListener(listener);
}
public Collection<TxOutput> values() {
return map.values();
}
public boolean isEmpty() {
return map.isEmpty();
}
public int size() {
return map.size();
}
public Set<Map.Entry<TxIdIndexTuple, TxOutput>> entrySet() {
return map.entrySet();
}
@Override
public String toString() {
return "BsqUTXOMap " + map.toString();
}
}

View File

@ -1,6 +0,0 @@
package io.bisq.core.dao.blockchain;
public interface BsqTxoListener {
void onBsqTxoChanged(BsqTXOMap bsqTXOMap);
}

View File

@ -1,70 +0,0 @@
/*
* This file is part of bisq.
*
* bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bisq.core.dao.blockchain;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import java.io.Serializable;
// Estimation for UTXO set: 1 UTXO object has 78 byte
// 1000 UTXOs - 10 000 UTXOs: 78kb -780kb
@Value
@Slf4j
public class BsqUTXO implements Serializable {
private final int height;
private final TxOutput txOutput;
private final boolean isBsqCoinBase;
private final String utxoId;
public BsqUTXO(int height, TxOutput txOutput, boolean isBsqCoinBase) {
this.height = height;
this.txOutput = txOutput;
this.isBsqCoinBase = isBsqCoinBase;
utxoId = txOutput.getTxId() + ":" + txOutput.getIndex();
}
public String getAddress() {
return txOutput.getAddress();
}
public long getValue() {
return txOutput.getValue();
}
public String getTxId() {
return txOutput.getTxId();
}
public int getIndex() {
return txOutput.getIndex();
}
@Override
public String toString() {
return "BsqUTXO{" +
"\n height=" + height +
", \n output=" + txOutput +
", \n isBsqCoinBase=" + isBsqCoinBase +
", \n utxoId='" + utxoId + '\'' +
"\n}";
}
}

View File

@ -1,6 +0,0 @@
package io.bisq.core.dao.blockchain;
public interface BsqUTXOListener {
void onBsqUTXOChanged(BsqUTXOMap bsqUTXOMap);
}

View File

@ -1,122 +0,0 @@
/*
* This file is part of Bitsquare.
*
* Bitsquare is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bisq.core.dao.blockchain;
import io.bisq.common.storage.Storage;
import javafx.collections.FXCollections;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableMap;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.*;
@Slf4j
public class BsqUTXOMap implements Serializable {
// We don't use a Lombok delegate here as we want control the access to our map
@Getter
private HashMap<TxIdIndexTuple, BsqUTXO> map = new HashMap<>();
@Getter
private HashSet<String> txIdSet = new HashSet<>();
@Getter
@Setter
private int snapshotHeight;
private transient ObservableMap<TxIdIndexTuple, BsqUTXO> observableMap;
private transient final Storage<BsqUTXOMap> storage;
public BsqUTXOMap(File storageDir) {
storage = new Storage<>(storageDir);
BsqUTXOMap persisted = storage.initAndGetPersisted(this, "BsqUTXOMap");
if (persisted != null) {
map.putAll(persisted.getMap());
txIdSet = persisted.getTxIdSet();
snapshotHeight = persisted.getSnapshotHeight();
}
observableMap = FXCollections.observableHashMap();
observableMap.putAll(map);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
try {
in.defaultReadObject();
observableMap = FXCollections.observableHashMap();
observableMap.putAll(map);
} catch (Throwable t) {
log.warn("Cannot be deserialized." + t.getMessage());
}
}
public Object add(BsqUTXO bsqUTXO) {
txIdSet.add(bsqUTXO.getTxId());
final BsqUTXO result = map.put(new TxIdIndexTuple(bsqUTXO.getTxId(), bsqUTXO.getIndex()), bsqUTXO);
observableMap.put(new TxIdIndexTuple(bsqUTXO.getTxId(), bsqUTXO.getIndex()), bsqUTXO);
return result;
}
public BsqUTXO removeByTuple(String txId, int index) {
txIdSet.remove(txId);
final BsqUTXO result = map.remove(new TxIdIndexTuple(txId, index));
observableMap.remove(new TxIdIndexTuple(txId, index));
return result;
}
public void persist() {
storage.queueUpForSave();
}
public boolean containsTuple(String txId, int index) {
return map.containsKey(new TxIdIndexTuple(txId, index));
}
public BsqUTXO getByTuple(String txId, int index) {
return observableMap.get(new TxIdIndexTuple(txId, index));
}
public void addListener(MapChangeListener<TxIdIndexTuple, BsqUTXO> listener) {
observableMap.addListener(listener);
}
public Collection<BsqUTXO> values() {
return map.values();
}
public boolean isEmpty() {
return map.isEmpty();
}
public int size() {
return map.size();
}
public Set<Map.Entry<TxIdIndexTuple, BsqUTXO>> entrySet() {
return map.entrySet();
}
@Override
public String toString() {
return "BsqUTXOMap " + map.toString();
}
}

View File

@ -62,7 +62,6 @@ public class TxOutput implements Serializable {
@Nullable
private String address;
public TxOutput(int index,
long value,
String txId,
@ -105,16 +104,35 @@ public class TxOutput implements Serializable {
return address;
}
public boolean isUnSpend() {
return spendInfo == null;
}
public boolean hasBurnedFee() {
return burnedFee > 0;
}
public String getTxoId() {
return txId + ":" + index;
}
@Override
public String toString() {
return "TxOutput{" +
"\n index=" + index +
",\n value=" + value +
",\n address=" + getAddress() +
",\n txId=" + txId +
",\n pubKeyScript=" + pubKeyScript +
",\n spendInfo=" + spendInfo +
"\n" +
" }";
"\n index=" + index +
",\n value=" + value +
",\n txId='" + txId + '\'' +
",\n pubKeyScript=" + pubKeyScript +
",\n blockHeight=" + blockHeight +
",\n time=" + time +
",\n txVersion='" + txVersion + '\'' +
",\n isBsqCoinBase=" + isBsqCoinBase +
",\n isVerified=" + isVerified +
",\n burnedFee=" + burnedFee +
",\n btcTxFee=" + btcTxFee +
",\n spendInfo=" + spendInfo +
",\n address='" + getAddress() + '\'' +
"\n}";
}
}

View File

@ -0,0 +1,134 @@
/*
* This file is part of Bitsquare.
*
* Bitsquare is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bisq.core.dao.blockchain;
import io.bisq.common.storage.Storage;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
// Map of any ever existing TxOutput which was a valid BSQ
@Slf4j
public class TxOutputMap implements Serializable {
///////////////////////////////////////////////////////////////////////////////////////////
// Interface
///////////////////////////////////////////////////////////////////////////////////////////
public interface Listener {
void onMapChanged(TxOutputMap txOutputMap);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Instance fields
///////////////////////////////////////////////////////////////////////////////////////////
// We don't use a Lombok delegate here as we want control the access to our map
@Getter
private HashMap<TxIdIndexTuple, TxOutput> map = new HashMap<>();
@Getter
@Setter
private int snapshotHeight = 0;
private transient final Storage<TxOutputMap> storage;
private final List<Listener> listeners = new ArrayList<>();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public TxOutputMap(File storageDir) {
storage = new Storage<>(storageDir);
TxOutputMap persisted = storage.initAndGetPersisted(this, "BsqTxOutputMap");
if (persisted != null) {
map.putAll(persisted.getMap());
snapshotHeight = persisted.getSnapshotHeight();
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Public methods
///////////////////////////////////////////////////////////////////////////////////////////
public boolean isTxOutputUnSpent(String txId, int index) {
final TxOutput txOutput = get(txId, index);
return txOutput != null && txOutput.isUnSpend();
}
public boolean hasTxBurnedFee(String txId) {
final TxOutput txOutput = get(txId, 0);
return txOutput != null && txOutput.hasBurnedFee();
}
public void persist() {
storage.queueUpForSave();
}
public void addListener(Listener listener) {
listeners.add(listener);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Delegated map methods
///////////////////////////////////////////////////////////////////////////////////////////
public Object put(TxOutput txOutput) {
final TxOutput result = map.put(new TxIdIndexTuple(txOutput.getTxId(), txOutput.getIndex()), txOutput);
listeners.stream().forEach(l -> l.onMapChanged(this));
return result;
}
@Nullable
public TxOutput get(String txId, int index) {
return map.get(new TxIdIndexTuple(txId, index));
}
public boolean contains(String txId, int index) {
return map.containsKey(new TxIdIndexTuple(txId, index));
}
public Collection<TxOutput> values() {
return map.values();
}
public boolean isEmpty() {
return map.isEmpty();
}
public int size() {
return map.size();
}
@Override
public String toString() {
return "BsqTxOutputMap " + map.toString();
}
}

View File

@ -0,0 +1,121 @@
/*
* This file is part of bisq.
*
* bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bisq.core.dao.blockchain.json;
import com.google.inject.Inject;
import com.neemre.btcdcli4j.core.domain.PubKeyScript;
import io.bisq.common.storage.PlainTextWrapper;
import io.bisq.common.storage.Storage;
import io.bisq.common.util.Utilities;
import io.bisq.core.dao.RpcOptionKeys;
import io.bisq.core.dao.blockchain.SpendInfo;
import io.bisq.core.dao.blockchain.TxOutput;
import io.bisq.core.dao.blockchain.TxOutputMap;
import javax.inject.Named;
import java.io.File;
import java.util.List;
import java.util.stream.Collectors;
public class JsonExporter {
private final Storage<PlainTextWrapper> jsonStorage;
private boolean dumpBlockchainData;
private final File storageDir;
@Inject
public JsonExporter(Storage<PlainTextWrapper> jsonStorage,
@Named(Storage.DIR_KEY) File storageDir,
@Named(RpcOptionKeys.DUMP_BLOCKCHAIN_DATA) boolean dumpBlockchainData) {
this.storageDir = storageDir;
this.jsonStorage = jsonStorage;
this.dumpBlockchainData = dumpBlockchainData;
}
public void init(@Named(RpcOptionKeys.DUMP_BLOCKCHAIN_DATA) boolean dumpBlockchainData) {
if (dumpBlockchainData) {
this.jsonStorage.initWithFileName("txo.json");
}
}
public void export(TxOutputMap txOutputMap) {
if (dumpBlockchainData) {
List<TxOutputForJson> list = txOutputMap.getMap().values().stream()
.map(this::getTxOutputForJson)
.collect(Collectors.toList());
list.sort((o1, o2) -> (o1.getSortData().compareTo(o2.getSortData())));
TxOutputForJson[] array = new TxOutputForJson[list.size()];
list.toArray(array);
jsonStorage.queueUpForSave(new PlainTextWrapper(Utilities.objectToJson(array)), 5000);
// keep the individual file storage option as code as we dont know yet what we will use.
/* log.error("txOutputForJson " + txOutputForJson);
File txoDir = new File(Paths.get(storageDir.getAbsolutePath(), "txo").toString());
if (!txoDir.exists())
if (!txoDir.mkdir())
log.warn("make txoDir failed.\ntxoDir=" + txoDir.getAbsolutePath());
File txoFile = new File(Paths.get(txoDir.getAbsolutePath(),
txOutput.getTxId() + ":" + outputIndex + ".json").toString());
// Nr of write requests might be a bit heavy, consider write whole list to one file
FileManager<PlainTextWrapper> fileManager = new FileManager<>(storageDir, txoFile, 1);
fileManager.saveLater(new PlainTextWrapper(Utilities.objectToJson(txOutputForJson)));*/
}
}
private TxOutputForJson getTxOutputForJson(TxOutput txOutput) {
String txId = txOutput.getTxId();
int outputIndex = txOutput.getIndex();
final long bsqAmount = txOutput.getValue();
final int height = txOutput.getBlockHeight();
final boolean isBsqCoinBase = txOutput.isBsqCoinBase();
final boolean verified = txOutput.isVerified();
final long burnedFee = txOutput.getBurnedFee();
final long btcTxFee = txOutput.getBtcTxFee();
PubKeyScript pubKeyScript = txOutput.getPubKeyScript();
final ScriptPubKeyForJson scriptPubKey = new ScriptPubKeyForJson(pubKeyScript.getAddresses(),
pubKeyScript.getAsm(),
pubKeyScript.getHex(),
pubKeyScript.getReqSigs(),
pubKeyScript.getType().toString());
SpentInfoForJson spentInfoJson = null;
SpendInfo spendInfo = txOutput.getSpendInfo();
if (spendInfo != null)
spentInfoJson = new SpentInfoForJson(spendInfo.getBlockHeight(),
spendInfo.getInputIndex(),
spendInfo.getTxId());
final long time = txOutput.getTime();
final String txVersion = txOutput.getTxVersion();
return new TxOutputForJson(txId,
outputIndex,
bsqAmount,
height,
isBsqCoinBase,
verified,
burnedFee,
btcTxFee,
scriptPubKey,
spentInfoJson,
time,
txVersion
);
}
}

View File

@ -31,13 +31,8 @@ import java.math.BigDecimal;
import java.net.URL;
import java.util.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.*;
/*
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
*/
@Slf4j
public class BsqBlockchainServiceTest {
@ -64,16 +59,14 @@ public class BsqBlockchainServiceTest {
public static final long ADDRESS_TX_2_VALUE = Coin.parseCoin("0.00001000").value;
private MockBsqBlockchainService service;
private BsqUTXOMap bsqUTXOMap;
private BsqTXOMap bsqTXOMap;
private TxOutputMap txOutputMap;
@Before
public void setup() {
final URL resource = this.getClass().getClassLoader().getResource("");
final String path = resource != null ? resource.getFile() : "";
log.info("path for BsqUTXOMap=" + path);
bsqUTXOMap = new BsqUTXOMap(new File(path));
bsqTXOMap = new BsqTXOMap(new File(path));
txOutputMap = new TxOutputMap(new File(path));
service = new MockBsqBlockchainService();
}
@ -98,13 +91,13 @@ public class BsqBlockchainServiceTest {
parseAllBlocksFromGenesis();
BsqUTXO bsqUTXO1 = bsqUTXOMap.getByTuple(GEN_TX_ID, 0);
BsqUTXO bsqUTXO2 = bsqUTXOMap.getByTuple(GEN_TX_ID, 1);
assertEquals(bsqUTXO1.getUtxoId(), getUTXOId(GEN_TX_ID, 0));
assertEquals(bsqUTXO2.getUtxoId(), getUTXOId(GEN_TX_ID, 1));
assertEquals(ADDRESS_GEN_1_VALUE, bsqUTXO1.getValue());
assertEquals(ADDRESS_GEN_2_VALUE, bsqUTXO2.getValue());
assertEquals(2, bsqUTXOMap.size());
TxOutput bsqTxo1 = txOutputMap.get(GEN_TX_ID, 0);
TxOutput bsqTxo2 = txOutputMap.get(GEN_TX_ID, 1);
assertEquals(bsqTxo1.getTxoId(), getTxoId(GEN_TX_ID, 0));
assertEquals(bsqTxo2.getTxoId(), getTxoId(GEN_TX_ID, 1));
assertEquals(ADDRESS_GEN_1_VALUE, bsqTxo1.getValue());
assertEquals(ADDRESS_GEN_2_VALUE, bsqTxo2.getValue());
assertEquals(2, txOutputMap.size());
}
@ -137,13 +130,15 @@ public class BsqBlockchainServiceTest {
parseAllBlocksFromGenesis();
BsqUTXO bsqUTXO1 = bsqUTXOMap.getByTuple(GEN_TX_ID, 0);
BsqUTXO bsqUTXO2 = bsqUTXOMap.getByTuple(TX1_ID, 0);
assertEquals(bsqUTXO1.getUtxoId(), getUTXOId(GEN_TX_ID, 0));
assertEquals(bsqUTXO2.getUtxoId(), getUTXOId(TX1_ID, 0));
assertEquals(ADDRESS_GEN_1_VALUE, bsqUTXO1.getValue());
assertEquals(ADDRESS_TX_1_VALUE, bsqUTXO2.getValue());
assertEquals(2, bsqUTXOMap.size());
TxOutput bsqTxo1 = txOutputMap.get(GEN_TX_ID, 0);
TxOutput bsqTxo2 = txOutputMap.get(TX1_ID, 0);
assertTrue(bsqTxo1.isUnSpend());
assertTrue(bsqTxo2.isUnSpend());
assertEquals(bsqTxo1.getTxoId(), getTxoId(GEN_TX_ID, 0));
assertEquals(bsqTxo2.getTxoId(), getTxoId(TX1_ID, 0));
assertEquals(ADDRESS_GEN_1_VALUE, bsqTxo1.getValue());
assertEquals(ADDRESS_TX_1_VALUE, bsqTxo2.getValue());
assertEquals(3, txOutputMap.size());
}
@Test
@ -189,13 +184,20 @@ public class BsqBlockchainServiceTest {
parseAllBlocksFromGenesis();
BsqUTXO bsqUTXO1 = bsqUTXOMap.getByTuple(GEN_TX_ID, 0);
BsqUTXO bsqUTXO2 = bsqUTXOMap.getByTuple(TX2_ID, 0);
assertEquals(bsqUTXO1.getUtxoId(), getUTXOId(GEN_TX_ID, 0));
assertEquals(bsqUTXO2.getUtxoId(), getUTXOId(TX2_ID, 0));
assertEquals(ADDRESS_GEN_1_VALUE, bsqUTXO1.getValue());
assertEquals(ADDRESS_TX_2_VALUE, bsqUTXO2.getValue());
assertEquals(2, bsqUTXOMap.size());
TxOutput bsqTxo1 = txOutputMap.get(GEN_TX_ID, 0);
TxOutput bsqTxo2 = txOutputMap.get(TX2_ID, 0);
txOutputMap.values().forEach(e -> {
if (e.equals(bsqTxo1) || e.equals(bsqTxo2))
assertTrue(e.isUnSpend());
else
assertFalse(e.isUnSpend());
});
assertEquals(bsqTxo1.getTxoId(), getTxoId(GEN_TX_ID, 0));
assertEquals(bsqTxo2.getTxoId(), getTxoId(TX2_ID, 0));
assertEquals(ADDRESS_GEN_1_VALUE, bsqTxo1.getValue());
assertEquals(ADDRESS_TX_2_VALUE, bsqTxo2.getValue());
assertEquals(4, txOutputMap.size());
}
@Test
@ -241,13 +243,19 @@ public class BsqBlockchainServiceTest {
parseAllBlocksFromGenesis();
BsqUTXO bsqUTXO1 = bsqUTXOMap.getByTuple(GEN_TX_ID, 0);
BsqUTXO bsqUTXO2 = bsqUTXOMap.getByTuple(TX2_ID, 0);
assertEquals(bsqUTXO1.getUtxoId(), getUTXOId(GEN_TX_ID, 0));
assertEquals(bsqUTXO2.getUtxoId(), getUTXOId(TX2_ID, 0));
assertEquals(ADDRESS_GEN_1_VALUE, bsqUTXO1.getValue());
assertEquals(ADDRESS_TX_2_VALUE, bsqUTXO2.getValue());
assertEquals(2, bsqUTXOMap.size());
TxOutput bsqTxo1 = txOutputMap.get(GEN_TX_ID, 0);
TxOutput bsqTxo2 = txOutputMap.get(TX2_ID, 0);
txOutputMap.values().forEach(e -> {
if (e.equals(bsqTxo1) || e.equals(bsqTxo2))
assertTrue(e.isUnSpend());
else
assertFalse(e.isUnSpend());
});
assertEquals(bsqTxo1.getTxoId(), getTxoId(GEN_TX_ID, 0));
assertEquals(bsqTxo2.getTxoId(), getTxoId(TX2_ID, 0));
assertEquals(ADDRESS_GEN_1_VALUE, bsqTxo1.getValue());
assertEquals(ADDRESS_TX_2_VALUE, bsqTxo2.getValue());
assertEquals(4, txOutputMap.size());
}
@Test
@ -295,10 +303,16 @@ public class BsqBlockchainServiceTest {
parseAllBlocksFromGenesis();
BsqUTXO bsqUTXO1 = bsqUTXOMap.getByTuple(TX2_ID, 0);
assertEquals(bsqUTXO1.getUtxoId(), getUTXOId(TX2_ID, 0));
assertEquals(ADDRESS_GEN_1_VALUE + ADDRESS_GEN_2_VALUE, bsqUTXO1.getValue());
assertEquals(1, bsqUTXOMap.size());
TxOutput bsqTxo1 = txOutputMap.get(TX2_ID, 0);
txOutputMap.values().forEach(e -> {
if (e.equals(bsqTxo1))
assertTrue(e.isUnSpend());
else
assertFalse(e.isUnSpend());
});
assertEquals(bsqTxo1.getTxoId(), getTxoId(TX2_ID, 0));
assertEquals(ADDRESS_GEN_1_VALUE + ADDRESS_GEN_2_VALUE, bsqTxo1.getValue());
assertEquals(4, txOutputMap.size());
}
@ -388,16 +402,16 @@ public class BsqBlockchainServiceTest {
private void parseAllBlocksFromGenesis()
throws BitcoindException, CommunicationException, BsqBlockchainException {
service.parseBlockchain(bsqUTXOMap,
bsqTXOMap,
BsqParser bsqParser = new BsqParser(service);
bsqParser.parseBlocks(BLOCK_0,
service.requestChainHeadHeight(),
BLOCK_0,
BLOCK_0,
GEN_TX_ID);
GEN_TX_ID,
txOutputMap);
}
private String getUTXOId(String TxId, int index) {
return TxId + ":" + index;
private String getTxoId(String txId, int index) {
return txId + ":" + index;
}
}
@ -416,7 +430,7 @@ class MockBsqBlockchainService extends BsqBlockchainRpcService {
private final Map<Integer, List<String>> txIdsInBlockMap = new HashMap<>();
public MockBsqBlockchainService() {
super(null, null, null, null, null);
super(null, null, null, null);
}
public void buildBlocks(int from, int to) {
@ -489,7 +503,7 @@ class MockBsqBlockchainService extends BsqBlockchainRpcService {
}
@Override
protected RawTransaction getRawTransaction(String txId) throws BitcoindException, CommunicationException {
RawTransaction requestRawTransaction(String txId) throws BitcoindException, CommunicationException {
return txByIdMap.get(txId);
}
}

View File

@ -19,8 +19,6 @@ package io.bisq.gui.main.dao.wallet;
import io.bisq.core.btc.wallet.BsqBalanceListener;
import io.bisq.core.btc.wallet.BsqWalletService;
import io.bisq.core.dao.blockchain.BsqBlockchainManager;
import io.bisq.core.dao.blockchain.BsqUTXOListener;
import io.bisq.gui.util.BsqFormatter;
import javafx.scene.control.TextField;
import lombok.extern.slf4j.Slf4j;
@ -32,15 +30,13 @@ import javax.inject.Inject;
public class BalanceUtil implements BsqBalanceListener {
private final BsqWalletService bsqWalletService;
private final BsqFormatter formatter;
private BsqBlockchainManager bsqBlockchainManager;
private TextField balanceTextField;
private BsqUTXOListener bsqUTXOListener;
@Inject
private BalanceUtil(BsqWalletService bsqWalletService, BsqFormatter formatter, BsqBlockchainManager bsqBlockchainManager) {
private BalanceUtil(BsqWalletService bsqWalletService,
BsqFormatter formatter) {
this.bsqWalletService = bsqWalletService;
this.formatter = formatter;
this.bsqBlockchainManager = bsqBlockchainManager;
}
public void setBalanceTextField(TextField balanceTextField) {
@ -49,18 +45,15 @@ public class BalanceUtil implements BsqBalanceListener {
}
public void initialize() {
bsqUTXOListener = bsqUTXOMap -> updateAvailableBalance(bsqWalletService.getAvailableBalance());
}
public void activate() {
updateAvailableBalance(bsqWalletService.getAvailableBalance());
bsqWalletService.addBsqBalanceListener(this);
bsqBlockchainManager.addUtxoListener(bsqUTXOListener);
}
public void deactivate() {
bsqWalletService.removeBsqBalanceListener(this);
bsqBlockchainManager.removeUtxoListener(bsqUTXOListener);
}
@Override

View File

@ -22,7 +22,6 @@ import io.bisq.common.locale.Res;
import io.bisq.core.btc.wallet.BsqWalletService;
import io.bisq.core.btc.wallet.BtcWalletService;
import io.bisq.core.dao.blockchain.BsqBlockchainManager;
import io.bisq.core.dao.blockchain.Tx;
import io.bisq.core.user.DontShowAgainLookup;
import io.bisq.core.user.Preferences;
import io.bisq.gui.common.view.ActivatableView;
@ -46,7 +45,8 @@ import javafx.util.Callback;
import org.bitcoinj.core.Transaction;
import javax.inject.Inject;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@ -112,7 +112,7 @@ public class BsqTxView extends ActivatableView<GridPane, Void> {
@Override
protected void activate() {
balanceUtil.activate();
bsqWalletService.getWalletBsqTransactions().addListener(walletBsqTransactionsListener);
bsqWalletService.getWalletTransactions().addListener(walletBsqTransactionsListener);
sortedList.comparatorProperty().bind(tableView.comparatorProperty());
tableView.setItems(sortedList);
@ -124,25 +124,30 @@ public class BsqTxView extends ActivatableView<GridPane, Void> {
protected void deactivate() {
balanceUtil.deactivate();
sortedList.comparatorProperty().unbind();
bsqWalletService.getWalletBsqTransactions().removeListener(walletBsqTransactionsListener);
bsqWalletService.getWalletTransactions().removeListener(walletBsqTransactionsListener);
observableList.forEach(BsqTxListItem::cleanup);
}
private void updateList() {
observableList.forEach(BsqTxListItem::cleanup);
Map<String, Tx> burnedBSQTxIdMap = bsqBlockchainManager.getBsqTXOMap().getBurnedBSQTxMap();
Set<BsqTxListItem> list = bsqWalletService.getWalletBsqTransactions().stream()
.map(transaction -> new BsqTxListItem(transaction,
bsqWalletService,
btcWalletService,
burnedBSQTxIdMap.containsKey(transaction.getHashAsString()), bsqFormatter)
// clone to avoid ConcurrentModificationException
final List<Transaction> walletTransactions = new ArrayList<>(bsqWalletService.getWalletTransactions());
Set<BsqTxListItem> list = walletTransactions.stream()
.map(transaction -> {
// The burned fee is added to all outputs of a tx, so we just ask at index 0
return new BsqTxListItem(transaction,
bsqWalletService,
btcWalletService,
bsqBlockchainManager.getTxOutputMap().hasTxBurnedFee(transaction.getHashAsString()),
bsqFormatter);
}
)
.collect(Collectors.toSet());
observableList.setAll(list);
final Set<Transaction> invalidBsqTransactions = bsqWalletService.getInvalidBsqTransactions();
if (!invalidBsqTransactions.isEmpty() && bsqBlockchainManager.isUtxoSyncWithChainHeadHeight()) {
if (!invalidBsqTransactions.isEmpty() && bsqBlockchainManager.isParseBlockchainComplete()) {
Set<String> txIds = invalidBsqTransactions.stream()
.filter(t -> t != null)
.map(t -> t.getHashAsString()).collect(Collectors.toSet());