From 65bbd85762dd65bce6257d349376b102d8ccd100 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Wed, 19 Dec 2018 14:00:45 +0100 Subject: [PATCH 1/5] Initialize Set to avoid nullpointer access --- .../java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java b/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java index eeee47cf8b0..65fafdfb99c 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java @@ -136,7 +136,7 @@ public class WithdrawalView extends ActivatableView { private final SortedList sortedList = new SortedList<>(observableList); private Set selectedItems = new HashSet<>(); private BalanceListener balanceListener; - private Set fromAddresses; + private Set fromAddresses = new HashSet<>(); private Coin totalAvailableAmountOfSelectedItems = Coin.ZERO; private Coin amountAsCoin = Coin.ZERO; private Coin sendersAmount = Coin.ZERO; From aabf318947cb4d502cfea05795731245be311973 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Thu, 13 Dec 2018 18:01:12 +0100 Subject: [PATCH 2/5] Add option to parse unconfirmed tx --- .../voteresult/issuance/IssuanceService.java | 2 +- .../core/dao/node/parser/BlockParser.java | 3 +- .../core/dao/node/parser/GenesisTxParser.java | 2 +- .../core/dao/node/parser/TxInputParser.java | 11 +++-- .../core/dao/node/parser/TxOutputParser.java | 7 +++- .../bisq/core/dao/node/parser/TxParser.java | 13 +++--- .../bisq/core/dao/state/DaoStateService.java | 30 +++++++++++--- .../dao/state/model/UnconfirmedState.java | 40 +++++++++++++++++++ .../core/dao/node/full/BlockParserTest.java | 6 +-- 9 files changed, 91 insertions(+), 23 deletions(-) create mode 100644 core/src/main/java/bisq/core/dao/state/model/UnconfirmedState.java diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/issuance/IssuanceService.java b/core/src/main/java/bisq/core/dao/governance/voteresult/issuance/IssuanceService.java index 77157f9e002..d4f178938e1 100644 --- a/core/src/main/java/bisq/core/dao/governance/voteresult/issuance/IssuanceService.java +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/issuance/IssuanceService.java @@ -80,7 +80,7 @@ public void issueBsq(IssuanceProposal issuanceProposal, int chainHeight) { String pubKey = txInput.getPubKey(); Issuance issuance = new Issuance(tx.getId(), chainHeight, amount, pubKey, issuanceType); daoStateService.addIssuance(issuance); - daoStateService.addUnspentTxOutput(txOutput); + daoStateService.addUnspentTxOutput(txOutput, true); StringBuilder sb = new StringBuilder(); sb.append("\n################################################################################\n"); diff --git a/core/src/main/java/bisq/core/dao/node/parser/BlockParser.java b/core/src/main/java/bisq/core/dao/node/parser/BlockParser.java index 5d71fd9164b..9dd10cffb5d 100644 --- a/core/src/main/java/bisq/core/dao/node/parser/BlockParser.java +++ b/core/src/main/java/bisq/core/dao/node/parser/BlockParser.java @@ -113,7 +113,8 @@ public Block parseBlock(RawBlock rawBlock) throws BlockHashNotConnectingExceptio txParser.findTx(rawTx, genesisTxId, genesisBlockHeight, - genesisTotalSupply) + genesisTotalSupply, + true) .ifPresent(txList::add)); log.info("parseBsqTxs took {} ms", rawBlock.getRawTxs().size(), System.currentTimeMillis() - startTs); diff --git a/core/src/main/java/bisq/core/dao/node/parser/GenesisTxParser.java b/core/src/main/java/bisq/core/dao/node/parser/GenesisTxParser.java index ae4aea62c72..00a84943edb 100644 --- a/core/src/main/java/bisq/core/dao/node/parser/GenesisTxParser.java +++ b/core/src/main/java/bisq/core/dao/node/parser/GenesisTxParser.java @@ -47,7 +47,7 @@ private static void commitUTXOs(DaoStateService daoStateService, TempTx genesisT ImmutableList outputs = genesisTx.getTempTxOutputs(); for (int i = 0; i < outputs.size(); ++i) { TempTxOutput tempTxOutput = outputs.get(i); - daoStateService.addUnspentTxOutput(TxOutput.fromTempOutput(tempTxOutput)); + daoStateService.addUnspentTxOutput(TxOutput.fromTempOutput(tempTxOutput), true); } } diff --git a/core/src/main/java/bisq/core/dao/node/parser/TxInputParser.java b/core/src/main/java/bisq/core/dao/node/parser/TxInputParser.java index 6f4b111bd9a..2023306bb0a 100644 --- a/core/src/main/java/bisq/core/dao/node/parser/TxInputParser.java +++ b/core/src/main/java/bisq/core/dao/node/parser/TxInputParser.java @@ -51,6 +51,7 @@ public class TxInputParser { // Private private int numVoteRevealInputs = 0; + private boolean confirmed; /////////////////////////////////////////////////////////////////////////////////////////// @@ -58,8 +59,10 @@ public class TxInputParser { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public TxInputParser(DaoStateService daoStateService) { + public TxInputParser(DaoStateService daoStateService, boolean confirmed) { + this.daoStateService = daoStateService; + this.confirmed = confirmed; } @@ -135,8 +138,10 @@ void process(TxOutputKey txOutputKey, int blockHeight, String txId, int inputInd break; } - daoStateService.setSpentInfo(connectedTxOutput.getKey(), new SpentInfo(blockHeight, txId, inputIndex)); - daoStateService.removeUnspentTxOutput(connectedTxOutput); + daoStateService.setSpentInfo(connectedTxOutput.getKey(), + new SpentInfo(blockHeight, txId, inputIndex), + confirmed); + daoStateService.removeUnspentTxOutput(connectedTxOutput, confirmed); }); } else { log.warn("Connected txOutput {} at input {} of txId {} is confiscated ", txOutputKey, inputIndex, txId); diff --git a/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java b/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java index 7a313adae81..8ca910e84fa 100644 --- a/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java +++ b/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java @@ -70,14 +70,16 @@ public class TxOutputParser { // Private private int lockTime; private final List utxoCandidates = new ArrayList<>(); + private boolean confirmed; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// - TxOutputParser(DaoStateService daoStateService) { + TxOutputParser(DaoStateService daoStateService, boolean confirmed) { this.daoStateService = daoStateService; + this.confirmed = confirmed; } @@ -132,7 +134,8 @@ void processTxOutput(TempTxOutput tempTxOutput) { } void commitUTXOCandidates() { - utxoCandidates.forEach(output -> daoStateService.addUnspentTxOutput(TxOutput.fromTempOutput(output))); + utxoCandidates.forEach(output -> + daoStateService.addUnspentTxOutput(TxOutput.fromTempOutput(output), confirmed)); } /** diff --git a/core/src/main/java/bisq/core/dao/node/parser/TxParser.java b/core/src/main/java/bisq/core/dao/node/parser/TxParser.java index 8badd3d168f..834caf1c217 100644 --- a/core/src/main/java/bisq/core/dao/node/parser/TxParser.java +++ b/core/src/main/java/bisq/core/dao/node/parser/TxParser.java @@ -68,11 +68,12 @@ public TxParser(PeriodService periodService, // API /////////////////////////////////////////////////////////////////////////////////////////// - public Optional findTx(RawTx rawTx, String genesisTxId, int genesisBlockHeight, Coin genesisTotalSupply) { - if (GenesisTxParser.isGenesis(rawTx, genesisTxId, genesisBlockHeight)) + public Optional findTx(RawTx rawTx, String genesisTxId, int genesisBlockHeight, Coin genesisTotalSupply, + boolean confirmed) { + if (confirmed && GenesisTxParser.isGenesis(rawTx, genesisTxId, genesisBlockHeight)) return Optional.of(GenesisTxParser.getGenesisTx(rawTx, genesisTotalSupply, daoStateService)); else - return findTx(rawTx); + return findTx(rawTx, confirmed); } // Apply state changes to tx, inputs and outputs @@ -80,7 +81,7 @@ public Optional findTx(RawTx rawTx, String genesisTxId, int genesisBlockHeig // Any tx with BSQ input is a BSQ tx. // There might be txs without any valid BSQ txOutput but we still keep track of it, // for instance to calculate the total burned BSQ. - private Optional findTx(RawTx rawTx) { + private Optional findTx(RawTx rawTx, boolean confirmed) { int blockHeight = rawTx.getBlockHeight(); TempTx tempTx = TempTx.fromRawTx(rawTx); @@ -88,7 +89,7 @@ private Optional findTx(RawTx rawTx) { // Parse Inputs //**************************************************************************************** - txInputParser = new TxInputParser(daoStateService); + txInputParser = new TxInputParser(daoStateService, confirmed); for (int inputIndex = 0; inputIndex < tempTx.getTxInputs().size(); inputIndex++) { TxInput input = tempTx.getTxInputs().get(inputIndex); TxOutputKey outputKey = input.getConnectedTxOutputKey(); @@ -114,7 +115,7 @@ private Optional findTx(RawTx rawTx) { // Parse Outputs //**************************************************************************************** - txOutputParser = new TxOutputParser(daoStateService); + txOutputParser = new TxOutputParser(daoStateService, confirmed); txOutputParser.setAvailableInputValue(accumulatedInputValue); txOutputParser.setUnlockBlockHeight(unlockBlockHeight); txOutputParser.setOptionalSpentLockupTxOutput(optionalSpentLockupTxOutput); diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateService.java b/core/src/main/java/bisq/core/dao/state/DaoStateService.java index dcadd3f7824..a5f0a0d01d5 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateService.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateService.java @@ -21,6 +21,7 @@ import bisq.core.dao.governance.bond.BondConsensus; import bisq.core.dao.governance.param.Param; import bisq.core.dao.state.model.DaoState; +import bisq.core.dao.state.model.UnconfirmedState; import bisq.core.dao.state.model.blockchain.Block; import bisq.core.dao.state.model.blockchain.SpentInfo; import bisq.core.dao.state.model.blockchain.Tx; @@ -65,6 +66,7 @@ @Slf4j public class DaoStateService implements DaoSetupService { private final DaoState daoState; + private final UnconfirmedState unconfirmedState; private final GenesisTxInfo genesisTxInfo; private final BsqFormatter bsqFormatter; private final List daoStateListeners = new CopyOnWriteArrayList<>(); @@ -77,6 +79,7 @@ public class DaoStateService implements DaoSetupService { @Inject public DaoStateService(DaoState daoState, GenesisTxInfo genesisTxInfo, BsqFormatter bsqFormatter) { this.daoState = daoState; + this.unconfirmedState = new UnconfirmedState(); this.genesisTxInfo = genesisTxInfo; this.bsqFormatter = bsqFormatter; } @@ -387,12 +390,22 @@ public Map getUnspentTxOutputMap() { return daoState.getUnspentTxOutputMap(); } - public void addUnspentTxOutput(TxOutput txOutput) { - getUnspentTxOutputMap().put(txOutput.getKey(), txOutput); + public void addUnspentTxOutput(TxOutput txOutput, boolean confirmed) { + if (confirmed) { + getUnspentTxOutputMap().put(txOutput.getKey(), txOutput); + } else { + unconfirmedState.getUnspentTxOutputMap().put(txOutput.getKey(),txOutput); + log.info("Unconfirmed txout added, txid=" + txOutput.getTxId()); + } } - public void removeUnspentTxOutput(TxOutput txOutput) { - getUnspentTxOutputMap().remove(txOutput.getKey()); + public void removeUnspentTxOutput(TxOutput txOutput, boolean confirmed) { + if (confirmed) { + getUnspentTxOutputMap().remove(txOutput.getKey()); + } else { + unconfirmedState.getUnspentTxOutputMap().remove(txOutput.getKey()); + log.info("Unconfirmed txout spent, txid=" + txOutput.getTxId()); + } } public boolean isUnspent(TxOutputKey key) { @@ -879,8 +892,13 @@ public String getParamValue(Param param, int blockHeight) { // SpentInfo /////////////////////////////////////////////////////////////////////////////////////////// - public void setSpentInfo(TxOutputKey txOutputKey, SpentInfo spentInfo) { - daoState.getSpentInfoMap().put(txOutputKey, spentInfo); + public void setSpentInfo(TxOutputKey txOutputKey, SpentInfo spentInfo, boolean confirmed) { + if (confirmed) { + daoState.getSpentInfoMap().put(txOutputKey, spentInfo); + } else { + unconfirmedState.getSpentInfoMap().put(txOutputKey, spentInfo); + log.info("Unconfirmed txout parsed, txid=" + txOutputKey.getTxId()); + } } public Optional getSpentInfo(TxOutput txOutput) { diff --git a/core/src/main/java/bisq/core/dao/state/model/UnconfirmedState.java b/core/src/main/java/bisq/core/dao/state/model/UnconfirmedState.java new file mode 100644 index 00000000000..4d359d60c7b --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/UnconfirmedState.java @@ -0,0 +1,40 @@ +/* + * 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 . + */ + +package bisq.core.dao.state.model; + +import bisq.core.dao.state.model.blockchain.SpentInfo; +import bisq.core.dao.state.model.blockchain.TxOutput; +import bisq.core.dao.state.model.blockchain.TxOutputKey; + +import java.util.HashMap; +import java.util.Map; + +import lombok.Getter; + +public class UnconfirmedState { + + @Getter + private final Map unspentTxOutputMap = new HashMap<>(); + @Getter + private final Map nonBsqTxOutputMap = new HashMap<>(); + @Getter + private final Map spentInfoMap = new HashMap<>(); + + public UnconfirmedState() { + } +} diff --git a/core/src/test/java/bisq/core/dao/node/full/BlockParserTest.java b/core/src/test/java/bisq/core/dao/node/full/BlockParserTest.java index b2bac9d193c..c6bd6e913f5 100644 --- a/core/src/test/java/bisq/core/dao/node/full/BlockParserTest.java +++ b/core/src/test/java/bisq/core/dao/node/full/BlockParserTest.java @@ -126,11 +126,11 @@ public void testIsBsqTx() { Coin genesisTotalSupply = Coin.parseCoin("2.5"); // First time there is no BSQ value to spend so it's not a bsq transaction - assertFalse(txParser.findTx(rawTx, genesisTxId, blockHeight, genesisTotalSupply).isPresent()); + assertFalse(txParser.findTx(rawTx, genesisTxId, blockHeight, genesisTotalSupply, true).isPresent()); // Second time there is BSQ in the first txout - assertTrue(txParser.findTx(rawTx, genesisTxId, blockHeight, genesisTotalSupply).isPresent()); + assertTrue(txParser.findTx(rawTx, genesisTxId, blockHeight, genesisTotalSupply, true).isPresent()); // Third time there is BSQ in the second txout - assertTrue(txParser.findTx(rawTx, genesisTxId, blockHeight, genesisTotalSupply).isPresent()); + assertTrue(txParser.findTx(rawTx, genesisTxId, blockHeight, genesisTotalSupply, true).isPresent()); } @Test From 8006be6a5cda9df0dbac25691925b2e440b9f286 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Thu, 20 Dec 2018 17:15:56 +0100 Subject: [PATCH 3/5] Allow spend of pending BSQ All pending txs are parsed by the TxParser but kept in UnconfirmedState. The confirmed state and unconfirmed state are aggregated on demand when getUnspentTxOutputMap is called. --- .../core/btc/wallet/BsqWalletService.java | 24 ++++++++++++++- ...ncompleteBitcoinjTransactionException.java | 26 +++++++++++++++++ .../java/bisq/core/dao/node/full/RawTx.java | 22 ++++++++++++++ .../bisq/core/dao/node/full/RawTxOutput.java | 21 ++++++++++++++ .../core/dao/node/parser/BlockParser.java | 3 +- .../core/dao/node/parser/TxOutputParser.java | 1 + .../bisq/core/dao/node/parser/TxParser.java | 14 +++++---- .../bisq/core/dao/state/DaoStateService.java | 29 +++++++++++++++---- .../dao/state/model/UnconfirmedState.java | 11 +++++++ .../dao/state/model/blockchain/TxInput.java | 11 +++++++ .../core/dao/node/full/BlockParserTest.java | 6 ++-- .../state/DaoStateSnapshotServiceTest.java | 3 ++ 12 files changed, 155 insertions(+), 16 deletions(-) create mode 100644 core/src/main/java/bisq/core/dao/exceptions/IncompleteBitcoinjTransactionException.java diff --git a/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java index 32572b3de5e..de13c4d6acb 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java @@ -23,6 +23,9 @@ import bisq.core.btc.exceptions.WalletException; import bisq.core.btc.listeners.BsqBalanceListener; import bisq.core.btc.setup.WalletsSetup; +import bisq.core.dao.exceptions.IncompleteBitcoinjTransactionException; +import bisq.core.dao.node.full.RawTx; +import bisq.core.dao.node.parser.TxParser; import bisq.core.dao.state.DaoStateListener; import bisq.core.dao.state.DaoStateService; import bisq.core.dao.state.model.blockchain.Block; @@ -56,6 +59,7 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -79,6 +83,7 @@ public class BsqWalletService extends WalletService implements DaoStateListener private final BsqCoinSelector bsqCoinSelector; private final NonBsqCoinSelector nonBsqCoinSelector; private final DaoStateService daoStateService; + private final TxParser txParser; private final ObservableList walletTransactions = FXCollections.observableArrayList(); private final CopyOnWriteArraySet bsqBalanceListeners = new CopyOnWriteArraySet<>(); @@ -106,6 +111,7 @@ public BsqWalletService(WalletsSetup walletsSetup, BsqCoinSelector bsqCoinSelector, NonBsqCoinSelector nonBsqCoinSelector, DaoStateService daoStateService, + TxParser txParser, Preferences preferences, FeeService feeService) { super(walletsSetup, @@ -115,6 +121,7 @@ public BsqWalletService(WalletsSetup walletsSetup, this.bsqCoinSelector = bsqCoinSelector; this.nonBsqCoinSelector = nonBsqCoinSelector; this.daoStateService = daoStateService; + this.txParser = txParser; if (BisqEnvironment.isBaseCurrencySupportingBsq()) { walletsSetup.addSetupCompletedHandler(() -> { @@ -307,10 +314,25 @@ public Stream getPendingWalletTransactionsStream() { private void updateBsqWalletTransactions() { walletTransactions.setAll(getTransactions(false)); - // walletTransactions.setAll(getBsqWalletTransactions()); + parsePending(); updateBsqBalance(); } + private void parsePending() { + // The transactions must be parsed in creation order since they might spend previous + // unconfirmed outputs + List orderedPending = getPendingWalletTransactionsStream() + .sorted((tx1, tx2) -> tx1.getUpdateTime().compareTo(tx2.getUpdateTime())) + .collect(Collectors.toList()); + orderedPending.stream() + .filter(tx -> { + // Don't try to parse pending orders unless all inputs are known + return tx.getInputs().stream().noneMatch(input -> input.getConnectedOutput() == null); + }) + .forEach(tx -> txParser.findTx( + RawTx.fromTransaction(tx, daoStateService.getBlockHeightOfLastBlock() + 1), false)); + } + private Set getBsqWalletTransactions() { return getTransactions(false).stream() .filter(transaction -> transaction.getConfidence().getConfidenceType() == PENDING || diff --git a/core/src/main/java/bisq/core/dao/exceptions/IncompleteBitcoinjTransactionException.java b/core/src/main/java/bisq/core/dao/exceptions/IncompleteBitcoinjTransactionException.java new file mode 100644 index 00000000000..1fae03ca804 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/exceptions/IncompleteBitcoinjTransactionException.java @@ -0,0 +1,26 @@ +/* + * 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 . + */ + +package bisq.core.dao.exceptions; + +public class IncompleteBitcoinjTransactionException extends Exception { + public final String txId; + + public IncompleteBitcoinjTransactionException(String txId) { + this.txId = txId; + } +} diff --git a/core/src/main/java/bisq/core/dao/node/full/RawTx.java b/core/src/main/java/bisq/core/dao/node/full/RawTx.java index 464e6432cf9..d176f787552 100644 --- a/core/src/main/java/bisq/core/dao/node/full/RawTx.java +++ b/core/src/main/java/bisq/core/dao/node/full/RawTx.java @@ -17,6 +17,7 @@ package bisq.core.dao.node.full; +import bisq.core.dao.exceptions.IncompleteBitcoinjTransactionException; import bisq.core.dao.state.model.blockchain.BaseTx; import bisq.core.dao.state.model.blockchain.Tx; import bisq.core.dao.state.model.blockchain.TxInput; @@ -26,6 +27,8 @@ import io.bisq.generated.protobuffer.PB; +import org.bitcoinj.core.Transaction; + import com.google.common.collect.ImmutableList; import java.util.ArrayList; @@ -62,6 +65,25 @@ public static RawTx fromTx(Tx tx) { rawTxOutputs); } + // Convert pending bitcoinj transaction to rawtx + public static RawTx fromTransaction(Transaction tx, int blockHeight) { + ImmutableList txInputs = ImmutableList.copyOf(tx.getInputs().stream() + .map(TxInput::fromTransactionInput) + .collect(Collectors.toList())); + + ImmutableList rawTxOutputs = ImmutableList.copyOf(tx.getOutputs().stream() + .map(RawTxOutput::fromTransactionOutput) + .collect(Collectors.toList())); + return new RawTx(String.valueOf(tx.getVersion()), + tx.getHashAsString(), + blockHeight, + "", + tx.getUpdateTime().getTime(), + txInputs, + rawTxOutputs + ); + } + private final ImmutableList rawTxOutputs; // The RPC service is creating a RawTx. diff --git a/core/src/main/java/bisq/core/dao/node/full/RawTxOutput.java b/core/src/main/java/bisq/core/dao/node/full/RawTxOutput.java index 9b20cc577ce..995fc3b808b 100644 --- a/core/src/main/java/bisq/core/dao/node/full/RawTxOutput.java +++ b/core/src/main/java/bisq/core/dao/node/full/RawTxOutput.java @@ -25,6 +25,13 @@ import io.bisq.generated.protobuffer.PB; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.script.ScriptChunk; + +import com.neemre.btcdcli4j.core.domain.Output; + +import java.util.List; + import lombok.EqualsAndHashCode; import lombok.Value; import lombok.extern.slf4j.Slf4j; @@ -51,6 +58,20 @@ public static RawTxOutput fromTxOutput(TxOutput txOutput) { txOutput.getBlockHeight()); } + // Convert unconfirmed bitcoinj outputs to rawtxoutput + // There will be no blockheight set yet for unconfirmed tx + public static RawTxOutput fromTransactionOutput(TransactionOutput output) { + List l = output.getScriptPubKey().isOpReturn() ? output.getScriptPubKey().getChunks() : null; + byte[] opret = output.getScriptPubKey().isOpReturn() ? output.getScriptPubKey().getChunks().get(1).data : null; + return new RawTxOutput(output.getIndex(), + output.getValue().getValue(), + output.getParentTransactionHash().toString(), + null, + null, + opret, + 0); + } + public RawTxOutput(int index, long value, String txId, diff --git a/core/src/main/java/bisq/core/dao/node/parser/BlockParser.java b/core/src/main/java/bisq/core/dao/node/parser/BlockParser.java index 9dd10cffb5d..5d71fd9164b 100644 --- a/core/src/main/java/bisq/core/dao/node/parser/BlockParser.java +++ b/core/src/main/java/bisq/core/dao/node/parser/BlockParser.java @@ -113,8 +113,7 @@ public Block parseBlock(RawBlock rawBlock) throws BlockHashNotConnectingExceptio txParser.findTx(rawTx, genesisTxId, genesisBlockHeight, - genesisTotalSupply, - true) + genesisTotalSupply) .ifPresent(txList::add)); log.info("parseBsqTxs took {} ms", rawBlock.getRawTxs().size(), System.currentTimeMillis() - startTs); diff --git a/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java b/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java index 8ca910e84fa..fcca11ecf6e 100644 --- a/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java +++ b/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java @@ -102,6 +102,7 @@ void processOpReturnOutput(TempTxOutput tempTxOutput) { } void processTxOutput(TempTxOutput tempTxOutput) { + // TODO(sq): this doens't make sense, how could a new output be confiscated? if (!daoStateService.isConfiscated(tempTxOutput.getKey())) { // We don not expect here an opReturn output as we do not get called on the last output. Any opReturn at // another output index is invalid. diff --git a/core/src/main/java/bisq/core/dao/node/parser/TxParser.java b/core/src/main/java/bisq/core/dao/node/parser/TxParser.java index 834caf1c217..06e17105c0b 100644 --- a/core/src/main/java/bisq/core/dao/node/parser/TxParser.java +++ b/core/src/main/java/bisq/core/dao/node/parser/TxParser.java @@ -68,12 +68,11 @@ public TxParser(PeriodService periodService, // API /////////////////////////////////////////////////////////////////////////////////////////// - public Optional findTx(RawTx rawTx, String genesisTxId, int genesisBlockHeight, Coin genesisTotalSupply, - boolean confirmed) { - if (confirmed && GenesisTxParser.isGenesis(rawTx, genesisTxId, genesisBlockHeight)) + public Optional findTx(RawTx rawTx, String genesisTxId, int genesisBlockHeight, Coin genesisTotalSupply) { + if (GenesisTxParser.isGenesis(rawTx, genesisTxId, genesisBlockHeight)) return Optional.of(GenesisTxParser.getGenesisTx(rawTx, genesisTotalSupply, daoStateService)); else - return findTx(rawTx, confirmed); + return findTx(rawTx, true); } // Apply state changes to tx, inputs and outputs @@ -81,7 +80,10 @@ public Optional findTx(RawTx rawTx, String genesisTxId, int genesisBlockHeig // Any tx with BSQ input is a BSQ tx. // There might be txs without any valid BSQ txOutput but we still keep track of it, // for instance to calculate the total burned BSQ. - private Optional findTx(RawTx rawTx, boolean confirmed) { + public Optional findTx(RawTx rawTx, boolean confirmed) { + if (!confirmed && !daoStateService.addUnconfirmed(rawTx.getId())) + return Optional.empty(); + int blockHeight = rawTx.getBlockHeight(); TempTx tempTx = TempTx.fromRawTx(rawTx); @@ -251,6 +253,8 @@ private void processIssuance(int blockHeight, TempTx tempTx, long bsqFee) { tempTx.setTxType(TxType.INVALID); } } else { + // TODO(sq:) This is harsh, an issuance canditate that didn't make into the proposal phase will have + // the entire BSQ input value to the propsal burnt tempTx.setTxType(TxType.INVALID); optionalIssuanceCandidate.ifPresent(tempTxOutput -> tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT)); // Empty Optional case is a possible valid case where a random tx matches our opReturn rules but it is not a diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateService.java b/core/src/main/java/bisq/core/dao/state/DaoStateService.java index a5f0a0d01d5..b8947e1c253 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateService.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateService.java @@ -44,6 +44,7 @@ import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -192,6 +193,7 @@ public Optional getStartHeightOfNextCycle(int blockHeight) { public void onNewBlockHeight(int blockHeight) { daoState.setChainHeight(blockHeight); daoStateListeners.forEach(listener -> listener.onNewBlockHeight(blockHeight)); + unconfirmedState.reset(); } // Second we get the block added with empty txs @@ -386,22 +388,29 @@ public Optional getTxOutput(TxOutputKey txOutputKey) { // UnspentTxOutput /////////////////////////////////////////////////////////////////////////////////////////// + // Returns copy of unspent tx map and unconfirmed unspent tx map public Map getUnspentTxOutputMap() { - return daoState.getUnspentTxOutputMap(); + Map unspent = new HashMap(); + unspent.putAll(daoState.getUnspentTxOutputMap()); + unconfirmedState.getSpentInfoMap().forEach((txOutputKey, spentInfo) -> { + unspent.remove(txOutputKey); + }); + unspent.putAll(unconfirmedState.getUnspentTxOutputMap()); + return unspent; } public void addUnspentTxOutput(TxOutput txOutput, boolean confirmed) { if (confirmed) { - getUnspentTxOutputMap().put(txOutput.getKey(), txOutput); + daoState.getUnspentTxOutputMap().put(txOutput.getKey(), txOutput); } else { - unconfirmedState.getUnspentTxOutputMap().put(txOutput.getKey(),txOutput); + unconfirmedState.getUnspentTxOutputMap().put(txOutput.getKey(), txOutput); log.info("Unconfirmed txout added, txid=" + txOutput.getTxId()); } } public void removeUnspentTxOutput(TxOutput txOutput, boolean confirmed) { if (confirmed) { - getUnspentTxOutputMap().remove(txOutput.getKey()); + daoState.getUnspentTxOutputMap().remove(txOutput.getKey()); } else { unconfirmedState.getUnspentTxOutputMap().remove(txOutput.getKey()); log.info("Unconfirmed txout spent, txid=" + txOutput.getTxId()); @@ -428,7 +437,6 @@ public boolean isTxOutputSpendable(TxOutputKey key) { // The above isUnspent call satisfies optionalTxOutput.isPresent() checkArgument(optionalTxOutput.isPresent(), "optionalTxOutput must be present"); TxOutput txOutput = optionalTxOutput.get(); - switch (txOutput.getTxOutputType()) { case UNDEFINED_OUTPUT: return false; @@ -935,6 +943,17 @@ public Set getProofOfBurnOpReturnTxOutputs() { return getTxOutputsByTxOutputType(TxOutputType.PROOF_OF_BURN_OP_RETURN_OUTPUT); } + /////////////////////////////////////////////////////////////////////////////////////////// + // Unconfirmed tx + /////////////////////////////////////////////////////////////////////////////////////////// + + public boolean addUnconfirmed(String txId) { + if (unconfirmedState.getParsedTxList().contains(txId)) + return false; + else + unconfirmedState.getParsedTxList().add(txId); + return true; + } /////////////////////////////////////////////////////////////////////////////////////////// // Listeners diff --git a/core/src/main/java/bisq/core/dao/state/model/UnconfirmedState.java b/core/src/main/java/bisq/core/dao/state/model/UnconfirmedState.java index 4d359d60c7b..9cdf41bdcd4 100644 --- a/core/src/main/java/bisq/core/dao/state/model/UnconfirmedState.java +++ b/core/src/main/java/bisq/core/dao/state/model/UnconfirmedState.java @@ -21,7 +21,9 @@ import bisq.core.dao.state.model.blockchain.TxOutput; import bisq.core.dao.state.model.blockchain.TxOutputKey; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import lombok.Getter; @@ -34,7 +36,16 @@ public class UnconfirmedState { private final Map nonBsqTxOutputMap = new HashMap<>(); @Getter private final Map spentInfoMap = new HashMap<>(); + @Getter + private final List parsedTxList = new ArrayList<>(); public UnconfirmedState() { } + + public void reset() { + unspentTxOutputMap.clear(); + nonBsqTxOutputMap.clear(); + spentInfoMap.clear(); + parsedTxList.clear(); + } } diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/TxInput.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/TxInput.java index ac1d676a7a7..23b59fe10e5 100644 --- a/core/src/main/java/bisq/core/dao/state/model/blockchain/TxInput.java +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/TxInput.java @@ -17,12 +17,15 @@ package bisq.core.dao.state.model.blockchain; +import bisq.core.dao.exceptions.IncompleteBitcoinjTransactionException; import bisq.core.dao.state.model.ImmutableDaoStateModel; import bisq.common.proto.persistable.PersistablePayload; import io.bisq.generated.protobuffer.PB; +import org.bitcoinj.core.TransactionInput; + import java.util.Optional; import lombok.EqualsAndHashCode; @@ -48,6 +51,14 @@ public static TxInput clone(TxInput txInput) { txInput.getPubKey()); } + // Parse unconfirmed bitcoinj tx + // Doesn't handle unconnected inputs, make sure to clean the inputs to be parsed + public static TxInput fromTransactionInput(TransactionInput input) { + return new TxInput(input.getConnectedOutput().getParentTransactionHash().toString(), + input.getConnectedOutput().getIndex(), + null); + } + private final String connectedTxOutputTxId; private final int connectedTxOutputIndex; @Nullable diff --git a/core/src/test/java/bisq/core/dao/node/full/BlockParserTest.java b/core/src/test/java/bisq/core/dao/node/full/BlockParserTest.java index c6bd6e913f5..b2bac9d193c 100644 --- a/core/src/test/java/bisq/core/dao/node/full/BlockParserTest.java +++ b/core/src/test/java/bisq/core/dao/node/full/BlockParserTest.java @@ -126,11 +126,11 @@ public void testIsBsqTx() { Coin genesisTotalSupply = Coin.parseCoin("2.5"); // First time there is no BSQ value to spend so it's not a bsq transaction - assertFalse(txParser.findTx(rawTx, genesisTxId, blockHeight, genesisTotalSupply, true).isPresent()); + assertFalse(txParser.findTx(rawTx, genesisTxId, blockHeight, genesisTotalSupply).isPresent()); // Second time there is BSQ in the first txout - assertTrue(txParser.findTx(rawTx, genesisTxId, blockHeight, genesisTotalSupply, true).isPresent()); + assertTrue(txParser.findTx(rawTx, genesisTxId, blockHeight, genesisTotalSupply).isPresent()); // Third time there is BSQ in the second txout - assertTrue(txParser.findTx(rawTx, genesisTxId, blockHeight, genesisTotalSupply, true).isPresent()); + assertTrue(txParser.findTx(rawTx, genesisTxId, blockHeight, genesisTotalSupply).isPresent()); } @Test diff --git a/core/src/test/java/bisq/core/dao/state/DaoStateSnapshotServiceTest.java b/core/src/test/java/bisq/core/dao/state/DaoStateSnapshotServiceTest.java index 61761af5f85..eccc6268931 100644 --- a/core/src/test/java/bisq/core/dao/state/DaoStateSnapshotServiceTest.java +++ b/core/src/test/java/bisq/core/dao/state/DaoStateSnapshotServiceTest.java @@ -17,6 +17,8 @@ package bisq.core.dao.state; +import bisq.core.dao.governance.period.CycleService; + import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; @@ -41,6 +43,7 @@ public class DaoStateSnapshotServiceTest { public void setup() { daoStateSnapshotService = new DaoStateSnapshotService(mock(DaoStateService.class), mock(GenesisTxInfo.class), + mock(CycleService.class), mock(DaoStateStorageService.class)); } From d17446e5bed4900c39a8f1491cb7e9ff0fb7cc67 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Tue, 25 Dec 2018 17:17:06 +0100 Subject: [PATCH 4/5] Fix #2123 --- core/src/main/java/bisq/core/dao/governance/bond/Bond.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/governance/bond/Bond.java b/core/src/main/java/bisq/core/dao/governance/bond/Bond.java index d4c8cd5df6b..b4d5ed612d5 100644 --- a/core/src/main/java/bisq/core/dao/governance/bond/Bond.java +++ b/core/src/main/java/bisq/core/dao/governance/bond/Bond.java @@ -53,8 +53,10 @@ protected Bond(T bondedAsset) { } public boolean isActive() { - return bondState != BondState.READY_FOR_LOCKUP && - bondState != BondState.UNLOCKED; + return bondState == BondState.LOCKUP_TX_CONFIRMED || + bondState == BondState.UNLOCK_TX_PENDING || + bondState == BondState.UNLOCK_TX_CONFIRMED || + bondState == BondState.UNLOCKING; } // Enums must not be used directly for hashCode or equals as it delivers the Object.hashCode (internal address)! From ec5f12dea3ba9f50d1d56e0e706080eb602ac449 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Tue, 25 Dec 2018 21:09:51 +0100 Subject: [PATCH 5/5] Don't burn BSQ for failed compensation request --- .../bisq/core/dao/node/parser/TxOutputParser.java | 1 - .../java/bisq/core/dao/node/parser/TxParser.java | 13 +++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java b/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java index fcca11ecf6e..8ca910e84fa 100644 --- a/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java +++ b/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java @@ -102,7 +102,6 @@ void processOpReturnOutput(TempTxOutput tempTxOutput) { } void processTxOutput(TempTxOutput tempTxOutput) { - // TODO(sq): this doens't make sense, how could a new output be confiscated? if (!daoStateService.isConfiscated(tempTxOutput.getKey())) { // We don not expect here an opReturn output as we do not get called on the last output. Any opReturn at // another output index is invalid. diff --git a/core/src/main/java/bisq/core/dao/node/parser/TxParser.java b/core/src/main/java/bisq/core/dao/node/parser/TxParser.java index 06e17105c0b..6ddfb9d2147 100644 --- a/core/src/main/java/bisq/core/dao/node/parser/TxParser.java +++ b/core/src/main/java/bisq/core/dao/node/parser/TxParser.java @@ -250,12 +250,13 @@ private void processIssuance(int blockHeight, TempTx tempTx, long bsqFee) { log.warn("It can be that we have a opReturn which is correct from its structure but the whole tx " + "in not valid as the issuanceCandidate in not there. " + "As the BSQ fee is set it must be either a buggy tx or an manually crafted invalid tx."); - tempTx.setTxType(TxType.INVALID); + tempTx.setTxType(TxType.UNDEFINED_TX_TYPE); } } else { - // TODO(sq:) This is harsh, an issuance canditate that didn't make into the proposal phase will have - // the entire BSQ input value to the propsal burnt - tempTx.setTxType(TxType.INVALID); + // This could be a valid compensation request that failed to be included in a block during the + // correct phase due to no fault of the user. Better not burn the change for as long as the BSQ inputs + // cover the value of the outputs. + tempTx.setTxType(TxType.UNDEFINED_TX_TYPE); optionalIssuanceCandidate.ifPresent(tempTxOutput -> tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT)); // Empty Optional case is a possible valid case where a random tx matches our opReturn rules but it is not a // valid BSQ tx. @@ -409,7 +410,7 @@ static TxType evaluateTxTypeFromOpReturnType(TempTx tempTx, OpReturnType opRetur boolean hasCorrectNumOutputs = tempTx.getTempTxOutputs().size() >= 3; if (!hasCorrectNumOutputs) { log.warn("Compensation/reimbursement request tx need to have at least 3 outputs"); - return TxType.INVALID; + return TxType.UNDEFINED_TX_TYPE; } TempTxOutput issuanceTxOutput = tempTx.getTempTxOutputs().get(1); @@ -417,7 +418,7 @@ static TxType evaluateTxTypeFromOpReturnType(TempTx tempTx, OpReturnType opRetur if (!hasIssuanceOutput) { log.warn("Compensation/reimbursement request txOutput type of output at index 1 need to be ISSUANCE_CANDIDATE_OUTPUT. " + "TxOutputType={}", issuanceTxOutput.getTxOutputType()); - return TxType.INVALID; + return TxType.UNDEFINED_TX_TYPE; } return opReturnType == OpReturnType.COMPENSATION_REQUEST ?