From 1df30463711798d59249afcd8d0c7dc598670b27 Mon Sep 17 00:00:00 2001 From: jmacxx <47253594+jmacxx@users.noreply.github.com> Date: Tue, 29 Nov 2022 18:06:39 -0600 Subject: [PATCH 01/89] Validate BSQ fee payment using DAO tx info. Previously the BSQ fee payment was determined by parsing a raw tx without relying on the DAO. Unfortunately this turned out to be problematic, so with this change the BSQ fee paid is obtained from the DAO tx, as originally preferred by chimp1984. Unconfirmed transactions will not be able to have their BSQ fees checked so early requests for validation will skip the fee check. This is not a problem since maker fee validation is done by the taker and in the majority of cases will be already confirmed; taker fee validation is done after the first confirm at trade step 2. --- .../core/provider/mempool/MempoolService.java | 32 ++++--- .../core/provider/mempool/TxValidator.java | 86 +++++------------ .../bisq/core/trade/model/bisq_v1/Trade.java | 4 + .../provider/mempool/TxValidatorTest.java | 92 ++++++++++++------- .../provider/mempool/badOfferTestData.json | 2 - .../pendingtrades/PendingTradesViewModel.java | 41 +++++---- 6 files changed, 128 insertions(+), 129 deletions(-) diff --git a/core/src/main/java/bisq/core/provider/mempool/MempoolService.java b/core/src/main/java/bisq/core/provider/mempool/MempoolService.java index d0dcf806e3..d86b052683 100644 --- a/core/src/main/java/bisq/core/provider/mempool/MempoolService.java +++ b/core/src/main/java/bisq/core/provider/mempool/MempoolService.java @@ -96,26 +96,36 @@ public class MempoolService { } public void validateOfferMakerTx(TxValidator txValidator, Consumer resultHandler) { - if (!isServiceSupported()) { - UserThread.runAfter(() -> resultHandler.accept(txValidator.endResult("mempool request not supported, bypassing", true)), 1); - return; + if (txValidator.getIsFeeCurrencyBtc() != null && txValidator.getIsFeeCurrencyBtc()) { + if (!isServiceSupported()) { + UserThread.runAfter(() -> resultHandler.accept(txValidator.endResult("mempool request not supported, bypassing", true)), 1); + return; + } + MempoolRequest mempoolRequest = new MempoolRequest(preferences, socks5ProxyProvider); + validateOfferMakerTx(mempoolRequest, txValidator, resultHandler); + } else { + // using BSQ for fees + UserThread.runAfter(() -> resultHandler.accept(txValidator.validateBsqFeeTx(true)), 1); } - MempoolRequest mempoolRequest = new MempoolRequest(preferences, socks5ProxyProvider); - validateOfferMakerTx(mempoolRequest, txValidator, resultHandler); } public void validateOfferTakerTx(Trade trade, Consumer resultHandler) { validateOfferTakerTx(new TxValidator(daoStateService, trade.getTakerFeeTxId(), trade.getAmount(), - trade.isCurrencyForTakerFeeBtc(), filterManager), resultHandler); + trade.isCurrencyForTakerFeeBtc(), trade.getLockTime(), filterManager), resultHandler); } public void validateOfferTakerTx(TxValidator txValidator, Consumer resultHandler) { - if (!isServiceSupported()) { - UserThread.runAfter(() -> resultHandler.accept(txValidator.endResult("mempool request not supported, bypassing", true)), 1); - return; + if (txValidator.getIsFeeCurrencyBtc() != null && txValidator.getIsFeeCurrencyBtc()) { + if (!isServiceSupported()) { + UserThread.runAfter(() -> resultHandler.accept(txValidator.endResult("mempool request not supported, bypassing", true)), 1); + return; + } + MempoolRequest mempoolRequest = new MempoolRequest(preferences, socks5ProxyProvider); + validateOfferTakerTx(mempoolRequest, txValidator, resultHandler); + } else { + // using BSQ for fees + resultHandler.accept(txValidator.validateBsqFeeTx(false)); } - MempoolRequest mempoolRequest = new MempoolRequest(preferences, socks5ProxyProvider); - validateOfferTakerTx(mempoolRequest, txValidator, resultHandler); } public void checkTxIsConfirmed(String txId, Consumer resultHandler) { diff --git a/core/src/main/java/bisq/core/provider/mempool/TxValidator.java b/core/src/main/java/bisq/core/provider/mempool/TxValidator.java index 566253eddc..10425dc1b8 100644 --- a/core/src/main/java/bisq/core/provider/mempool/TxValidator.java +++ b/core/src/main/java/bisq/core/provider/mempool/TxValidator.java @@ -19,6 +19,7 @@ package bisq.core.provider.mempool; import bisq.core.dao.governance.param.Param; import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Tx; import bisq.core.filter.FilterManager; import bisq.common.util.Tuple2; @@ -56,12 +57,11 @@ public class TxValidator { private final DaoStateService daoStateService; private final FilterManager filterManager; - private long blockHeightAtOfferCreation; // Only set for maker. + private long feePaymentBlockHeight; // applicable to maker and taker fees private final List errorList; private final String txId; private Coin amount; - @Nullable - private Boolean isFeeCurrencyBtc = null; + private Boolean isFeeCurrencyBtc = true; @Nullable private Long chainHeight; @Setter @@ -85,13 +85,13 @@ public class TxValidator { String txId, Coin amount, @Nullable Boolean isFeeCurrencyBtc, - long blockHeightAtOfferCreation, + long feePaymentBlockHeight, FilterManager filterManager) { this.daoStateService = daoStateService; this.txId = txId; this.amount = amount; this.isFeeCurrencyBtc = isFeeCurrencyBtc; - this.blockHeightAtOfferCreation = blockHeightAtOfferCreation; + this.feePaymentBlockHeight = feePaymentBlockHeight; this.filterManager = filterManager; this.errorList = new ArrayList<>(); this.jsonTxt = ""; @@ -119,8 +119,6 @@ public class TxValidator { if (checkNotNull(isFeeCurrencyBtc)) { status = checkFeeAddressBTC(jsonTxt, btcFeeReceivers) && checkFeeAmountBTC(jsonTxt, amount, true, getBlockHeightForFeeCalculation(jsonTxt)); - } else { - status = checkFeeAmountBSQ(jsonTxt, amount, true, getBlockHeightForFeeCalculation(jsonTxt)); } } } catch (JsonSyntaxException e) { @@ -132,6 +130,17 @@ public class TxValidator { return endResult("Maker tx validation", status); } + public TxValidator validateBsqFeeTx(boolean isMaker) { + Optional tx = daoStateService.getTx(txId); + String statusStr = isMaker ? "Maker" : "Taker" + " tx validation"; + if (tx.isEmpty()) { + log.info("DAO does not yet have the tx {}, bypassing check of burnt BSQ amount.", txId); + return endResult(statusStr, true); + } else { + return endResult(statusStr, checkFeeAmountBSQ(tx.get(), amount, isMaker, feePaymentBlockHeight)); + } + } + public TxValidator parseJsonValidateTakerFeeTx(String jsonTxt, List btcFeeReceivers) { this.jsonTxt = jsonTxt; boolean status = initialSanityChecks(txId, jsonTxt); @@ -143,8 +152,6 @@ public class TxValidator { if (isFeeCurrencyBtc) { status = checkFeeAddressBTC(jsonTxt, btcFeeReceivers) && checkFeeAmountBTC(jsonTxt, amount, false, getBlockHeightForFeeCalculation(jsonTxt)); - } else { - status = checkFeeAmountBSQ(jsonTxt, amount, false, getBlockHeightForFeeCalculation(jsonTxt)); } } } catch (JsonSyntaxException e) { @@ -250,28 +257,16 @@ public class TxValidator { return false; } - // I think its better to postpone BSQ fee check once the BSQ trade fee tx is confirmed and then use the BSQ explorer to request the - // BSQ fee to check if it is correct. - // Otherwise the requirements here become very complicated and potentially impossible to verify as we don't know - // if inputs and outputs are valid BSQ without the BSQ parser and confirmed transactions. - private boolean checkFeeAmountBSQ(String jsonTxt, Coin tradeAmount, boolean isMaker, long blockHeight) { - JsonArray jsonVin = getVinAndVout(jsonTxt).first; - JsonArray jsonVout = getVinAndVout(jsonTxt).second; - JsonObject jsonVin0 = jsonVin.get(0).getAsJsonObject(); - JsonObject jsonVout0 = jsonVout.get(0).getAsJsonObject(); - JsonElement jsonVIn0Value = jsonVin0.getAsJsonObject("prevout").get("value"); - JsonElement jsonVOut0Value = jsonVout0.getAsJsonObject().get("value"); - if (jsonVIn0Value == null || jsonVOut0Value == null) { - throw new JsonSyntaxException("vin/vout missing data"); - } + private boolean checkFeeAmountBSQ(Tx bsqTx, Coin tradeAmount, boolean isMaker, long blockHeight) { Param minFeeParam = isMaker ? Param.MIN_MAKER_FEE_BSQ : Param.MIN_TAKER_FEE_BSQ; long expectedFeeAsLong = calculateFee(tradeAmount, isMaker ? getMakerFeeRateBsq(blockHeight) : getTakerFeeRateBsq(blockHeight), minFeeParam).getValue(); - long feeValue = getBsqBurnt(jsonVin, jsonVOut0Value.getAsLong(), expectedFeeAsLong); + + long feeValue = bsqTx.getBurntBsq(); log.debug("BURNT BSQ maker fee: {} BSQ ({} sats)", (double) feeValue / 100.0, feeValue); - String description = String.format("Expected fee: %.2f BSQ, actual fee paid: %.2f BSQ", - (double) expectedFeeAsLong / 100.0, (double) feeValue / 100.0); + String description = String.format("Expected fee: %.2f BSQ, actual fee paid: %.2f BSQ, Trade amount: %s", + (double) expectedFeeAsLong / 100.0, (double) feeValue / 100.0, tradeAmount.toPlainString()); if (expectedFeeAsLong == feeValue) { log.debug("The fee matched. " + description); @@ -279,7 +274,7 @@ public class TxValidator { } if (expectedFeeAsLong < feeValue) { - log.info("The fee was more than what we expected. " + description); + log.info("The fee was more than what we expected. " + description + " Tx:" + bsqTx.getId()); return true; } @@ -350,39 +345,6 @@ public class TxValidator { // we don't care if it is confirmed or not, just that it exists. } - // a BSQ maker/taker fee transaction looks like this: - // BSQ INPUT 1 BSQ OUTPUT - // BSQ INPUT 2 BTC OUTPUT FOR RESERVED AMOUNT - // BSQ INPUT n BTC OUTPUT FOR CHANGE - // BTC INPUT 1 - // BTC INPUT 2 - // BTC INPUT n - // there can be any number of BSQ inputs and BTC inputs - // BSQ inputs always come first in the tx, followed by BTC for the collateral. - // the sum of all BSQ inputs minus the BSQ output is the burnt amount, or trading fee. - long getBsqBurnt(JsonArray jsonVin, long bsqOutValue, long expectedFee) { - // sum consecutive inputs until we have accumulated enough to cover the output + burnt amount - long bsqInValue = 0; - for (int txIndex = 0; txIndex < jsonVin.size() - 1; txIndex++) { - bsqInValue += jsonVin.get(txIndex).getAsJsonObject().getAsJsonObject("prevout").get("value").getAsLong(); - if (bsqInValue - expectedFee >= bsqOutValue) { - break; // target reached - bsq input exceeds the output and expected burn amount - } - } - // guard against negative burn amount (i.e. only 1 tx input, or first in < first out) - long burntAmount = Math.max(0, bsqInValue - bsqOutValue); - // since we do not know which of the first 'n' are definitively BSQ inputs, sanity-check that the burnt amount - // is not too ridiculously high, as that would imply that we counted a BTC input. - if (burntAmount > 10 * expectedFee) { - log.error("The apparent BSQ fee burnt seems ridiculously high ({}) compared to expected ({})", burntAmount, expectedFee); - burntAmount = 0; // returning zero will flag the trade for manual review - } - if (burntAmount == 0) { - log.error("Could not obtain the burnt BSQ amount, trade will be flagged for manual review."); - } - return burntAmount; - } - private static long getTxConfirms(String jsonTxt, long chainHeight) { long blockHeight = getTxBlockHeight(jsonTxt); if (blockHeight > 0) { @@ -395,8 +357,8 @@ public class TxValidator { // if the tx is not yet confirmed, use current block tip, if tx is confirmed use the block it was confirmed at. private long getBlockHeightForFeeCalculation(String jsonTxt) { // For the maker we set the blockHeightAtOfferCreation from the offer - if (blockHeightAtOfferCreation > 0) { - return blockHeightAtOfferCreation; + if (feePaymentBlockHeight > 0) { + return feePaymentBlockHeight; } long txBlockHeight = getTxBlockHeight(jsonTxt); diff --git a/core/src/main/java/bisq/core/trade/model/bisq_v1/Trade.java b/core/src/main/java/bisq/core/trade/model/bisq_v1/Trade.java index 35b6048ecc..67face6768 100644 --- a/core/src/main/java/bisq/core/trade/model/bisq_v1/Trade.java +++ b/core/src/main/java/bisq/core/trade/model/bisq_v1/Trade.java @@ -897,6 +897,10 @@ public abstract class Trade extends TradeModel { return new Date(getTradeStartTime() + getMaxTradePeriod()); } + public long getTradeAge() { + return System.currentTimeMillis() - getTradeStartTime(); + } + private long getMaxTradePeriod() { return offer.getPaymentMethod().getMaxTradePeriod(); } diff --git a/core/src/test/java/bisq/core/provider/mempool/TxValidatorTest.java b/core/src/test/java/bisq/core/provider/mempool/TxValidatorTest.java index 793941e35a..d8e1fcca95 100644 --- a/core/src/test/java/bisq/core/provider/mempool/TxValidatorTest.java +++ b/core/src/test/java/bisq/core/provider/mempool/TxValidatorTest.java @@ -19,6 +19,7 @@ package bisq.core.provider.mempool; import bisq.core.dao.governance.param.Param; import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Tx; import bisq.core.filter.Filter; import bisq.core.filter.FilterManager; import bisq.core.trade.DelayedPayoutAddressProvider; @@ -39,6 +40,7 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import org.slf4j.Logger; @@ -77,30 +79,35 @@ public class TxValidatorTest { public void testMakerTx() { String mempoolData, offerData; + log.info("checking issue from user 2022-10-07"); + offerData = "1322804,5bec4007de1cb8cf18a5fa859d80d66031b8c78cfd99674e09ffd65cf23b50fc,9630000,137,0,757500"; + mempoolData = "{\"txid\":\"5bec4007de1cb8cf18a5fa859d80d66031b8c78cfd99674e09ffd65cf23b50fc\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":8921}},{\"vout\":1,\"prevout\":{\"value\":12155000}},{\"vout\":1,\"prevout\":{\"value\":2967000}}],\"vout\":[{\"scriptpubkey_address\":\"bc1qtyl6dququ2amxtsh4f3kx5rk9f5w9cuscz7ugm\",\"value\":8784},{\"scriptpubkey_address\":\"bc1qwj0jktuyjwj2ecwp9wgcrztxhve0hwn7n5lnxg\",\"value\":12519000},{\"scriptpubkey_address\":\"bc1qn3rd52mzkp6mgduz5wxprjw4rk9xpft6kga2mk\",\"value\":2600037}],\"size\":551,\"weight\":1229,\"fee\":3100,\"status\":{\"confirmed\":true,\"block_height\":757528,\"block_hash\":\"00000000000000000006b4426a0d2688a7e933e563e4d4fd80f572d935b12ae9\",\"block_time\":1665150690}}"; + Assert.assertTrue(createTxValidator(offerData).parseJsonValidateMakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + log.info("expected: paid the correct amount of BSQ fees"); offerData = "msimscqb,0636bafb14890edfb95465e66e2b1e15915f7fb595f9b653b9129c15ef4c1c4b,1000000,10,0,662390"; mempoolData = "{\"txid\":\"0636bafb14890edfb95465e66e2b1e15915f7fb595f9b653b9129c15ef4c1c4b\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":7899}},{\"vout\":2,\"prevout\":{\"value\":54877439}}],\"vout\":[{\"scriptpubkey_address\":\"1FCUu7hqKCSsGhVJaLbGEoCWdZRJRNqq8w\",\"value\":7889},{\"scriptpubkey_address\":\"bc1qkj5l4wxl00ufdx6ygcnrck9fz5u927gkwqcgey\",\"value\":1600000},{\"scriptpubkey_address\":\"bc1qkw4a8u9l5w9fhdh3ue9v7e7celk4jyudzg5gk5\",\"value\":53276799}],\"size\":405,\"weight\":1287,\"fee\":650,\"status\":{\"confirmed\":true,\"block_height\":663140}}"; Assert.assertTrue(createTxValidator(offerData).parseJsonValidateMakerFeeTx(mempoolData, btcFeeReceivers).getResult()); log.info("expected: paid the correct amount of BSQ fees with two UTXOs"); - offerData = "qmmtead,94b2589f3270caa0df63437707d4442cae34498ee5b0285090deed9c0ce8584d,800000,10,0,705301"; + offerData = "qmmtead,94b2589f3270caa0df63437707d4442cae34498ee5b0285090deed9c0ce8584d,800000,11,0,705301"; mempoolData = "{\"txid\":\"94b2589f3270caa0df63437707d4442cae34498ee5b0285090deed9c0ce8584d\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":577}},{\"vout\":0,\"prevout\":{\"value\":19989}},{\"vout\":2,\"prevout\":{\"value\":3008189}}],\"vout\":[{\"scriptpubkey_address\":\"bc1q48p2nvqf3tepjy7x33c5sfx3tp89e8c05z46cs\",\"value\":20555},{\"scriptpubkey_address\":\"bc1q9h69k8l0vy2yv3c72lw2cgn95sd7hlwjjzul05\",\"value\":920000},{\"scriptpubkey_address\":\"bc1qxmwscy2krw7zzfryw5g8868dexfy6pnq9yx3rv\",\"value\":2085750}],\"size\":550,\"weight\":1228,\"fee\":2450,\"status\":{\"confirmed\":true,\"block_height\":705301}}"; Assert.assertTrue(createTxValidator(offerData).parseJsonValidateMakerFeeTx(mempoolData, btcFeeReceivers).getResult()); log.info("expected: UNDERPAID expected 1.01 BSQ, actual fee paid 0.80 BSQ (USED 8.00 RATE INSTEAD OF 10.06 RATE"); offerData = "48067552,3b6009da764b71d79a4df8e2d8960b6919cae2e9bdccd5ef281e261fa9cd31b3,10000000,80,0,667656"; - mempoolData = "{\"txid\":\"3b6009da764b71d79a4df8e2d8960b6919cae2e9bdccd5ef281e261fa9cd31b3\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":9717}},{\"vout\":0,\"prevout\":{\"value\":4434912}},{\"vout\":2,\"prevout\":{\"value\":12809932}}],\"vout\":[{\"scriptpubkey_address\":\"1Nzqa4J7ck5bgz7QNXKtcjZExAvReozFo4\",\"value\":9637},{\"scriptpubkey_address\":\"bc1qhmmulf5prreqhccqy2wqpxxn6dcye7ame9dd57\",\"value\":11500000},{\"scriptpubkey_address\":\"bc1qx6hg8km2jdjc5ukhuedmkseu9wlsjtd8zeajpj\",\"value\":5721894}],\"size\":553,\"weight\":1879,\"fee\":23030,\"status\":{\"confirmed\":true,\"block_height\":667660}}"; - Assert.assertFalse(createTxValidator(offerData).parseJsonValidateMakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + //mempoolData = "{\"txid\":\"3b6009da764b71d79a4df8e2d8960b6919cae2e9bdccd5ef281e261fa9cd31b3\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":9717}},{\"vout\":0,\"prevout\":{\"value\":4434912}},{\"vout\":2,\"prevout\":{\"value\":12809932}}],\"vout\":[{\"scriptpubkey_address\":\"1Nzqa4J7ck5bgz7QNXKtcjZExAvReozFo4\",\"value\":9637},{\"scriptpubkey_address\":\"bc1qhmmulf5prreqhccqy2wqpxxn6dcye7ame9dd57\",\"value\":11500000},{\"scriptpubkey_address\":\"bc1qx6hg8km2jdjc5ukhuedmkseu9wlsjtd8zeajpj\",\"value\":5721894}],\"size\":553,\"weight\":1879,\"fee\":23030,\"status\":{\"confirmed\":true,\"block_height\":667660}}"; + Assert.assertFalse(createTxValidator(offerData).validateBsqFeeTx(true).getResult()); log.info("expected: UNDERPAID Expected fee: 0.61 BSQ, actual fee paid: 0.35 BSQ (USED 5.75 RATE INSTEAD OF 10.06 RATE"); offerData = "am7DzIv,4cdea8872a7d96210f378e0221dc1aae8ee9abb282582afa7546890fb39b7189,6100000,35,0,668195"; - mempoolData = "{\"txid\":\"4cdea8872a7d96210f378e0221dc1aae8ee9abb282582afa7546890fb39b7189\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":23893}},{\"vout\":1,\"prevout\":{\"value\":1440000}},{\"vout\":2,\"prevout\":{\"value\":16390881}}],\"vout\":[{\"scriptpubkey_address\":\"1Kmrzq3WGCQsZw5kroEphuk1KgsEr65yB7\",\"value\":23858},{\"scriptpubkey_address\":\"bc1qyw5qql9m7rkse9mhcun225nrjpwycszsa5dpjg\",\"value\":7015000},{\"scriptpubkey_address\":\"bc1q90y3p6mg0pe3rvvzfeudq4mfxafgpc9rulruff\",\"value\":10774186}],\"size\":554,\"weight\":1559,\"fee\":41730,\"status\":{\"confirmed\":true,\"block_height\":668198}}"; - Assert.assertFalse(createTxValidator(offerData).parseJsonValidateMakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + //mempoolData = "{\"txid\":\"4cdea8872a7d96210f378e0221dc1aae8ee9abb282582afa7546890fb39b7189\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":23893}},{\"vout\":1,\"prevout\":{\"value\":1440000}},{\"vout\":2,\"prevout\":{\"value\":16390881}}],\"vout\":[{\"scriptpubkey_address\":\"1Kmrzq3WGCQsZw5kroEphuk1KgsEr65yB7\",\"value\":23858},{\"scriptpubkey_address\":\"bc1qyw5qql9m7rkse9mhcun225nrjpwycszsa5dpjg\",\"value\":7015000},{\"scriptpubkey_address\":\"bc1q90y3p6mg0pe3rvvzfeudq4mfxafgpc9rulruff\",\"value\":10774186}],\"size\":554,\"weight\":1559,\"fee\":41730,\"status\":{\"confirmed\":true,\"block_height\":668198}}"; + Assert.assertFalse(createTxValidator(offerData).validateBsqFeeTx(true).getResult()); log.info("expected: UNDERPAID expected 0.11 BSQ, actual fee paid 0.08 BSQ (USED 5.75 RATE INSTEAD OF 7.53"); offerData = "F1dzaFNQ,f72e263947c9dee6fbe7093fc85be34a149ef5bcfdd49b59b9cc3322fea8967b,1440000,8,0,670822, bsq paid too little"; - mempoolData = "{\"txid\":\"f72e263947c9dee6fbe7093fc85be34a149ef5bcfdd49b59b9cc3322fea8967b\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":15163}},{\"vout\":2,\"prevout\":{\"value\":6100000}}],\"vout\":[{\"scriptpubkey_address\":\"1MEsc2m4MSomNJWSr1p6fhnUQMyA3DRGrN\",\"value\":15155},{\"scriptpubkey_address\":\"bc1qztgwe9ry9a9puchjuscqdnv4v9lsm2ut0jtfec\",\"value\":2040000},{\"scriptpubkey_address\":\"bc1q0nstwxc0vqkj4x000xt328mfjapvlsd56nn70h\",\"value\":4048308}],\"size\":406,\"weight\":1291,\"fee\":11700,\"status\":{\"confirmed\":true,\"block_height\":670823}}"; - Assert.assertFalse(createTxValidator(offerData).parseJsonValidateMakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + //mempoolData = "{\"txid\":\"f72e263947c9dee6fbe7093fc85be34a149ef5bcfdd49b59b9cc3322fea8967b\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":15163}},{\"vout\":2,\"prevout\":{\"value\":6100000}}],\"vout\":[{\"scriptpubkey_address\":\"1MEsc2m4MSomNJWSr1p6fhnUQMyA3DRGrN\",\"value\":15155},{\"scriptpubkey_address\":\"bc1qztgwe9ry9a9puchjuscqdnv4v9lsm2ut0jtfec\",\"value\":2040000},{\"scriptpubkey_address\":\"bc1q0nstwxc0vqkj4x000xt328mfjapvlsd56nn70h\",\"value\":4048308}],\"size\":406,\"weight\":1291,\"fee\":11700,\"status\":{\"confirmed\":true,\"block_height\":670823}}"; + Assert.assertFalse(createTxValidator(offerData).validateBsqFeeTx(true).getResult()); } @Test @@ -115,22 +122,17 @@ public class TxValidatorTest { log.info("========== test case: The fee matched what we expected (BSQ)"); offerData = "00072328,12f658954890d38ce698355be0b27fdd68d092c7b1b7475381918db060f46166,6250000,188,0,615955"; mempoolData = "{\"txid\":\"12f658954890d38ce698355be0b27fdd68d092c7b1b7475381918db060f46166\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":19980}},{\"vout\":2,\"prevout\":{\"value\":2086015}},{\"vout\":0,\"prevout\":{\"value\":1100000}},{\"vout\":2,\"prevout\":{\"value\":938200}}],\"vout\":[{\"scriptpubkey_address\":\"17qiF1TYgT1YvsCPJyXQoKMtBZ7YJBW9GH\",\"value\":19792},{\"scriptpubkey_address\":\"16aFKD5hvEjJgPme5yRNJT2rAPdTXzdQc2\",\"value\":3768432},{\"scriptpubkey_address\":\"1D5V3QW8f5n4PhwfPgNkW9eWZwNJFyVU8n\",\"value\":346755}],\"size\":701,\"weight\":2804,\"fee\":9216,\"status\":{\"confirmed\":true,\"block_height\":615955}}"; - Assert.assertTrue(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + Assert.assertTrue(createTxValidator(offerData).validateBsqFeeTx(false).getResult()); log.info("========== test case: No BSQ was burnt (error)"); - offerData = "NOBURN,12f658954890d38ce698355be0b27fdd68d092c7b1b7475381918db060f46166,6250000,188,0,615955"; + offerData = "NOBURN,12f658954890d38ce698355be0b27fdd68d092c7b1b7475381918db060f46166,6250000,0,0,615955"; mempoolData = "{\"txid\":\"12f658954890d38ce698355be0b27fdd68d092c7b1b7475381918db060f46166\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":19980}},{\"vout\":2,\"prevout\":{\"value\":2086015}},{\"vout\":0,\"prevout\":{\"value\":1100000}},{\"vout\":2,\"prevout\":{\"value\":938200}}],\"vout\":[{\"scriptpubkey_address\":\"17qiF1TYgT1YvsCPJyXQoKMtBZ7YJBW9GH\",\"value\":19980},{\"scriptpubkey_address\":\"16aFKD5hvEjJgPme5yRNJT2rAPdTXzdQc2\",\"value\":3768432},{\"scriptpubkey_address\":\"1D5V3QW8f5n4PhwfPgNkW9eWZwNJFyVU8n\",\"value\":346755}],\"size\":701,\"weight\":2804,\"fee\":9216,\"status\":{\"confirmed\":true,\"block_height\":615955}}"; - Assert.assertFalse(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + Assert.assertFalse(createTxValidator(offerData).validateBsqFeeTx(false).getResult()); log.info("========== test case: No BSQ input (error)"); - offerData = "NOBSQ,12f658954890d38ce698355be0b27fdd68d092c7b1b7475381918db060f46166,6250000,188,0,615955"; + offerData = "NOBSQ,12f658954890d38ce698355be0b27fdd68d092c7b1b7475381918db060f46166,6250000,0,0,615955"; mempoolData = "{\"txid\":\"12f658954890d38ce698355be0b27fdd68d092c7b1b7475381918db060f46166\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":2,\"prevout\":{\"value\":2086015}},{\"vout\":0,\"prevout\":{\"value\":1100000}},{\"vout\":2,\"prevout\":{\"value\":938200}}],\"vout\":[{\"scriptpubkey_address\":\"16aFKD5hvEjJgPme5yRNJT2rAPdTXzdQc2\",\"value\":3768432},{\"scriptpubkey_address\":\"1D5V3QW8f5n4PhwfPgNkW9eWZwNJFyVU8n\",\"value\":346755}],\"size\":701,\"weight\":2804,\"fee\":9216,\"status\":{\"confirmed\":true,\"block_height\":615955}}"; - Assert.assertFalse(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); - - log.info("========== test case: only one input (error)"); - offerData = "1INPUT,12f658954890d38ce698355be0b27fdd68d092c7b1b7475381918db060f46166,6250000,188,0,615955"; - mempoolData = "{\"txid\":\"12f658954890d38ce698355be0b27fdd68d092c7b1b7475381918db060f46166\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":2,\"prevout\":{\"value\":4186015}}],\"vout\":[{\"scriptpubkey_address\":\"16aFKD5hvEjJgPme5yRNJT2rAPdTXzdQc2\",\"value\":3768432},{\"scriptpubkey_address\":\"1D5V3QW8f5n4PhwfPgNkW9eWZwNJFyVU8n\",\"value\":346755}],\"size\":701,\"weight\":2804,\"fee\":9216,\"status\":{\"confirmed\":true,\"block_height\":615955}}"; - Assert.assertFalse(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + Assert.assertFalse(createTxValidator(offerData).validateBsqFeeTx(false).getResult()); log.info("========== test case: The fee was what we expected: (7000 sats)"); offerData = "bsqtrade,dfa4555ab78c657cad073e3f29c38c563d9dafc53afaa8c6af28510c734305c4,1000000,10,1,662390"; @@ -138,14 +140,14 @@ public class TxValidatorTest { Assert.assertTrue(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); log.info("========== test case: The fee matched what we expected"); - offerData = "89284,e1269aad63b3d894f5133ad658960971ef5c0fce6a13ad10544dc50fa3360588,900000,9,0,666473"; + offerData = "89284,e1269aad63b3d894f5133ad658960971ef5c0fce6a13ad10544dc50fa3360588,900000,47,0,666473"; mempoolData = "{\"txid\":\"e1269aad63b3d894f5133ad658960971ef5c0fce6a13ad10544dc50fa3360588\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":72738}},{\"vout\":0,\"prevout\":{\"value\":1600000}}],\"vout\":[{\"scriptpubkey_address\":\"17Kh5Ype9yNomqRrqu2k1mdV5c6FcKfGwQ\",\"value\":72691},{\"scriptpubkey_address\":\"bc1qdr9zcw7gf2sehxkux4fmqujm5uguhaqz7l9lca\",\"value\":629016},{\"scriptpubkey_address\":\"bc1qgqrrqv8q6l5d3t52fe28ghuhz4xqrsyxlwn03z\",\"value\":956523}],\"size\":404,\"weight\":1286,\"fee\":14508,\"status\":{\"confirmed\":true,\"block_height\":672388}}"; Assert.assertTrue(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); log.info("========== test case for UNDERPAID: Expected fee: 7.04 BSQ, actual fee paid: 1.01 BSQ"); offerData = "VOxRS,e99ea06aefc824fd45031447f7a0b56efb8117a09f9b8982e2c4da480a3a0e91,10000000,101,0,669129"; mempoolData = "{\"txid\":\"e99ea06aefc824fd45031447f7a0b56efb8117a09f9b8982e2c4da480a3a0e91\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":16739}},{\"vout\":2,\"prevout\":{\"value\":113293809}}],\"vout\":[{\"scriptpubkey_address\":\"1F14nF6zoUfJkqZrFgdmK5VX5QVwEpAnKW\",\"value\":16638},{\"scriptpubkey_address\":\"bc1q80y688ev7u43vqy964yf7feqddvt2mkm8977cm\",\"value\":11500000},{\"scriptpubkey_address\":\"bc1q9whgyc2du9mrgnxz0nl0shwpw8ugrcae0j0w8p\",\"value\":101784485}],\"size\":406,\"weight\":1291,\"fee\":9425,\"status\":{\"confirmed\":true,\"block_height\":669134}}"; - Assert.assertFalse(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + Assert.assertFalse(createTxValidator(offerData).validateBsqFeeTx(false).getResult()); log.info("========== test case for UNDERPAID: Expected fee: 1029000 sats BTC, actual fee paid: 441000 sats BTC because they used the default rate of 0.003 should have been 0.007 per BTC"); // after 1.6.0 we introduce additional leniency to allow the default rate (which is not stored in the DAO param change list) @@ -155,29 +157,29 @@ public class TxValidatorTest { Assert.assertFalse(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); log.info("========== test case for UNDERPAID: Expected fee: 2.12 BSQ, actual fee paid: 0.03 BSQ -- this is the example from the BSQ fee scammer Oct 2021"); - offerData = "957500,26e1a5e1f842cb7baa18bd197bd084e7f043d07720b9853e947158eb0a32677d,2000000,101,0,709426"; + offerData = "957500,26e1a5e1f842cb7baa18bd197bd084e7f043d07720b9853e947158eb0a32677d,2000000,101,3,709426"; mempoolData = "{\"txid\":\"26e1a5e1f842cb7baa18bd197bd084e7f043d07720b9853e947158eb0a32677d\",\"version\":1,\"locktime\":0,\"vin\":[{\"txid\":\"\",\"vout\":0,\"prevout\":{\"scriptpubkey\":\"\",\"scriptpubkey_asm\":\"\",\"scriptpubkey_type\":\"v0_p2wpkh\",\"scriptpubkey_address\":\"\",\"value\":3688},\"scriptsig\":\"\",\"scriptsig_asm\":\"\",\"witness\":[\"\",\"\"],\"is_coinbase\":false,\"sequence\":4294967295},{\"txid\":\"\",\"vout\":2,\"prevout\":{\"scriptpubkey\":\"\",\"scriptpubkey_asm\":\"\",\"scriptpubkey_type\":\"v0_p2wpkh\",\"scriptpubkey_address\":\"\",\"value\":796203},\"scriptsig\":\"\",\"scriptsig_asm\":\"\",\"witness\":[\"\",\"\"],\"is_coinbase\":false,\"sequence\":4294967295}],\"vout\":[{\"scriptpubkey\":\"\",\"scriptpubkey_asm\":\"\",\"scriptpubkey_type\":\"v0_p2wpkh\",\"scriptpubkey_address\":\"bc1qydcyfe7kp6968hywcp0uek2xvgem3nlx0x0hfy\",\"value\":3685},{\"scriptpubkey\":\"\",\"scriptpubkey_asm\":\"\",\"scriptpubkey_type\":\"v0_p2wpkh\",\"scriptpubkey_address\":\"bc1qc4amk6sd3c4gzxjgd5sdlaegt0r5juq54vnrll\",\"value\":503346},{\"scriptpubkey\":\"\",\"scriptpubkey_asm\":\"\",\"scriptpubkey_type\":\"v0_p2wpkh\",\"scriptpubkey_address\":\"bc1q66e7m8y5lzfk5smg2a80xeaqzhslgeavg9y70t\",\"value\":291187}],\"size\":403,\"weight\":958,\"fee\":1673,\"status\":{\"confirmed\":true,\"block_height\":709426,\"block_hash\":\"\",\"block_time\":1636751288}}"; - Assert.assertFalse(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + Assert.assertFalse(createTxValidator(offerData).validateBsqFeeTx(false).getResult()); log.info("========== test case: expected fee paid using two BSQ UTXOs"); - offerData = "ZHNYCAE,a91c6f1cb62721a7943678547aa814d6f29125ed63ad076073eb5ae7f16a76e9,83000000,101,0,717000"; + offerData = "ZHNYCAE,a91c6f1cb62721a7943678547aa814d6f29125ed63ad076073eb5ae7f16a76e9,83000000,8796,0,717000"; mempoolData = "{\"txid\":\"a91c6f1cb62721a7943678547aa814d6f29125ed63ad076073eb5ae7f16a76e9\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":3510}},{\"vout\":0,\"prevout\":{\"value\":6190}},{\"vout\":0,\"prevout\":{\"value\":46000000}}],\"vout\":[{\"scriptpubkey_address\":\"bc1qmqphx028eu4tzdvgccf5re52qtv6pmjanrpq29\",\"value\":904},{\"scriptpubkey_address\":\"bc1qtkvu4zeh0g0pce452335tgnswxd8ayxlktfj2s\",\"value\":30007648},{\"scriptpubkey_address\":\"bc1qdatwgzrrntp2m53tpzmax4dxu6md2c0c9vj8ut\",\"value\":15997324}],\"size\":549,\"weight\":1227,\"fee\":3824,\"status\":{\"confirmed\":true,\"block_height\":716444}}"; - Assert.assertTrue(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + Assert.assertTrue(createTxValidator(offerData).validateBsqFeeTx(false).getResult()); log.info("========== test case: expected fee paid using three BSQ UTXOs"); - offerData = "3UTXOGOOD,c7dddc267a366fa1d87840eeb0dcd89918a886ccb9aabee80f667635a5d4e262,200000000,101,0,733715"; + offerData = "3UTXOGOOD,c7dddc267a366fa1d87840eeb0dcd89918a886ccb9aabee80f667635a5d4e262,200000000,17888,0,733715"; mempoolData = "{\"txid\":\"c7dddc267a366fa1d87840eeb0dcd89918a886ccb9aabee80f667635a5d4e262\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":9833}},{\"vout\":0,\"prevout\":{\"value\":1362}},{vout\":0,\"prevout\":{\"value\":17488}},{\"vout\":2,\"prevout\":{\"value\":573360131}}],\"vout\":[{\"scriptpubkey_address\":\"bc1qvwpm87kmrlgave9srxk6nfwleehll0kxetu5j0\",\"value\":10795},{\"scriptpubkey_address\":\"bc1qz5n83ppfpdznnzff4e7tjep5c6f6jce9mqnrzh\",\"value\":230004780},{\"scriptpubkey_address\":\"bc1qcfyjajhuv55fyu6g5ug664r57u9a7qg55cgt5p\",\"value\":343370849}],\"size\":699,\"weight\":1500,\"fee\":2390,\"status\":{\"confirmed\":true,\"block_height\":733715}}"; - Assert.assertTrue(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + Assert.assertTrue(createTxValidator(offerData).validateBsqFeeTx(false).getResult()); log.info("========== test case: expected fee paid using four BSQ UTXOs"); - offerData = "4UTXOGOOD,c7dddc267a366fa1d87840eeb0dcd89918a886ccb9aabee80f667635a5d4e262,200000000,101,0,733715"; + offerData = "4UTXOGOOD,c7dddc267a366fa1d87840eeb0dcd89918a886ccb9aabee80f667635a5d4e262,200000000,17888,0,733715"; mempoolData = "{\"txid\":\"c7dddc267a366fa1d87840eeb0dcd89918a886ccb9aabee80f667635a5d4e262\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":4833}},{\"vout\":0,\"prevout\":{\"value\":5000}},{\"vout\":0,\"prevout\":{\"value\":1362}},{vout\":0,\"prevout\":{\"value\":17488}},{\"vout\":2,\"prevout\":{\"value\":573360131}}],\"vout\":[{\"scriptpubkey_address\":\"bc1qvwpm87kmrlgave9srxk6nfwleehll0kxetu5j0\",\"value\":10795},{\"scriptpubkey_address\":\"bc1qz5n83ppfpdznnzff4e7tjep5c6f6jce9mqnrzh\",\"value\":230004780},{\"scriptpubkey_address\":\"bc1qcfyjajhuv55fyu6g5ug664r57u9a7qg55cgt5p\",\"value\":343370849}],\"size\":699,\"weight\":1500,\"fee\":2390,\"status\":{\"confirmed\":true,\"block_height\":733715}}"; - Assert.assertTrue(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + Assert.assertTrue(createTxValidator(offerData).validateBsqFeeTx(false).getResult()); log.info("========== test case: three BSQ UTXOs, but fee paid is too low"); offerData = "3UTXOLOWFEE,c7dddc267a366fa1d87840eeb0dcd89918a886ccb9aabee80f667635a5d4e262,200000000,101,0,733715"; mempoolData = "{\"txid\":\"c7dddc267a366fa1d87840eeb0dcd89918a886ccb9aabee80f667635a5d4e262\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":9833}},{\"vout\":0,\"prevout\":{\"value\":1362}},{vout\":0,\"prevout\":{\"value\":1362}},{\"vout\":2,\"prevout\":{\"value\":573360131}}],\"vout\":[{\"scriptpubkey_address\":\"bc1qvwpm87kmrlgave9srxk6nfwleehll0kxetu5j0\",\"value\":10795},{\"scriptpubkey_address\":\"bc1qz5n83ppfpdznnzff4e7tjep5c6f6jce9mqnrzh\",\"value\":230004780},{\"scriptpubkey_address\":\"bc1qcfyjajhuv55fyu6g5ug664r57u9a7qg55cgt5p\",\"value\":343370849}],\"size\":699,\"weight\":1500,\"fee\":2390,\"status\":{\"confirmed\":true,\"block_height\":733715}}"; - Assert.assertFalse(createTxValidator(offerData).parseJsonValidateTakerFeeTx(mempoolData, btcFeeReceivers).getResult()); + Assert.assertFalse(createTxValidator(offerData).validateBsqFeeTx(false).getResult()); } @@ -207,12 +209,17 @@ public class TxValidatorTest { knownValuesList.forEach(offerData -> { TxValidator txValidator = createTxValidator(offerData); log.warn("TESTING {}", txValidator.getTxId()); - String jsonTxt = mempoolData.get(txValidator.getTxId()); - if (jsonTxt == null || jsonTxt.isEmpty()) { - log.warn("{} was not found in the mempool", txValidator.getTxId()); - Assert.assertFalse(expectedResult); // tx was not found in explorer + if (txValidator.getIsFeeCurrencyBtc()) { + String jsonTxt = mempoolData.get(txValidator.getTxId()); + if (jsonTxt == null || jsonTxt.isEmpty()) { + log.warn("{} was not found in the mempool", txValidator.getTxId()); + Assert.assertFalse(expectedResult); // tx was not found in explorer + } else { + txValidator.parseJsonValidateMakerFeeTx(jsonTxt, btcFeeReceivers); + Assert.assertTrue(expectedResult == txValidator.getResult()); + } } else { - txValidator.parseJsonValidateMakerFeeTx(jsonTxt, btcFeeReceivers); + txValidator.validateBsqFeeTx(true); Assert.assertTrue(expectedResult == txValidator.getResult()); } }); @@ -236,8 +243,11 @@ public class TxValidatorTest { String[] y = offerData.split(","); String txId = y[1]; long amount = Long.parseLong(y[2]); + long feePaid = Long.parseLong(y[3]); boolean isCurrencyForMakerFeeBtc = Long.parseLong(y[4]) > 0; + long feePaymentBlockHeight = Long.parseLong(y[5]); DaoStateService mockedDaoStateService = mock(DaoStateService.class); + Tx mockedTx = mock(Tx.class); Answer mockGetFeeRate = invocation -> { return mockedLookupFeeRate(invocation.getArgument(0), invocation.getArgument(1)); @@ -248,9 +258,17 @@ public class TxValidatorTest { Answer> mockGetParamChangeList = invocation -> { return mockedGetParamChangeList(invocation.getArgument(0)); }; + Answer> mockGetBsqTx = invocation -> { + return Optional.of(mockedTx); + }; + Answer mockGetBurntBsq = invocation -> { + return feePaid; + }; when(mockedDaoStateService.getParamValueAsCoin(Mockito.any(Param.class), Mockito.anyInt())).thenAnswer(mockGetFeeRate); when(mockedDaoStateService.getParamValueAsCoin(Mockito.any(Param.class), Mockito.anyString())).thenAnswer(mockGetParamValueAsCoin); when(mockedDaoStateService.getParamChangeList(Mockito.any())).thenAnswer(mockGetParamChangeList); + when(mockedDaoStateService.getTx(Mockito.any())).thenAnswer(mockGetBsqTx); + when(mockedTx.getBurntBsq()).thenAnswer(mockGetBurntBsq); Answer getMakerFeeBsq = invocation -> 1514L; Answer getTakerFeeBsq = invocation -> 10597L; @@ -263,7 +281,7 @@ public class TxValidatorTest { when(mockedFilter.getTakerFeeBtc()).thenAnswer(getTakerFeeBtc); FilterManager filterManager = mock(FilterManager.class); when(filterManager.getFilter()).thenReturn(mockedFilter); - TxValidator txValidator = new TxValidator(mockedDaoStateService, txId, Coin.valueOf(amount), isCurrencyForMakerFeeBtc, filterManager); + TxValidator txValidator = new TxValidator(mockedDaoStateService, txId, Coin.valueOf(amount), isCurrencyForMakerFeeBtc, feePaymentBlockHeight, filterManager); return txValidator; } catch (RuntimeException ignore) { // If input format is not as expected we ignore entry @@ -291,6 +309,8 @@ public class TxValidatorTest { private LinkedHashMap mockedGetFeeRateMap(Param param) { LinkedHashMap feeMap = new LinkedHashMap<>(); if (param == Param.DEFAULT_MAKER_FEE_BSQ) { + feeMap.put(754620L, "14.21"); // https://github.com/bisq-network/proposals/issues/380 + feeMap.put(750000L, "17.19"); // https://github.com/bisq-network/proposals/issues/379 feeMap.put(721063L, "12.78"); // https://github.com/bisq-network/proposals/issues/357 feeMap.put(706305L, "15.14"); // https://github.com/bisq-network/proposals/issues/345 feeMap.put(697011L, "13.16"); // https://github.com/bisq-network/proposals/issues/339 @@ -308,8 +328,10 @@ public class TxValidatorTest { feeMap.put(585787L, "8.0"); feeMap.put(581107L, "1.6"); } else if (param == Param.DEFAULT_TAKER_FEE_BSQ) { + feeMap.put(754620L, "104.24"); // https://github.com/bisq-network/proposals/issues/380 + feeMap.put(750000L, "126.02"); // https://github.com/bisq-network/proposals/issues/379 feeMap.put(721063L, "89.44"); // https://github.com/bisq-network/proposals/issues/357 - feeMap.put(706305L, "105.97"); // https://github.com/bisq-network/proposals/issues/345 + feeMap.put(706305L, "105.97"); // https://github.com/bisq-network/proposals/issues/345 feeMap.put(697011L, "92.15"); // https://github.com/bisq-network/proposals/issues/339 feeMap.put(682901L, "80.13"); // https://github.com/bisq-network/proposals/issues/333 feeMap.put(677862L, "69.68"); // https://github.com/bisq-network/proposals/issues/325 diff --git a/core/src/test/resources/bisq/core/provider/mempool/badOfferTestData.json b/core/src/test/resources/bisq/core/provider/mempool/badOfferTestData.json index c48788399a..836b471104 100644 --- a/core/src/test/resources/bisq/core/provider/mempool/badOfferTestData.json +++ b/core/src/test/resources/bisq/core/provider/mempool/badOfferTestData.json @@ -2,9 +2,7 @@ "ef1ea38b46402deb7df08c13a6dc379a65542a6940ac9d4ba436641ffd4bcb6e": "FQ0A7G,ef1ea38b46402deb7df08c13a6dc379a65542a6940ac9d4ba436641ffd4bcb6e,15970000,92,0,640438, underpaid but accepted due to use of different DAO parameter", "4cdea8872a7d96210f378e0221dc1aae8ee9abb282582afa7546890fb39b7189": "am7DzIv,4cdea8872a7d96210f378e0221dc1aae8ee9abb282582afa7546890fb39b7189,6100000,35,0,668195, underpaid but accepted due to use of different DAO parameter", "051770f8d7f43a9b6ca10fefa6cdf4cb124a81eed26dc8af2e40f57d2589107b": "046698,051770f8d7f43a9b6ca10fefa6cdf4cb124a81eed26dc8af2e40f57d2589107b,15970000,92,0,667927, bsq fee underpaid using 5.75 rate for some weird reason", - "37fba8bf119c289481eef031c0a35e126376f71d13d7cce35eb0d5e05799b5da": "hUWPf,37fba8bf119c289481eef031c0a35e126376f71d13d7cce35eb0d5e05799b5da,19910000,200,0,668994, tx_missing_from_blockchain_for_4_days", "b3bc726aa2aa6533cb1e61901ce351eecde234378fe650aee267388886aa6e4b": "ebdttmzh,b3bc726aa2aa6533cb1e61901ce351eecde234378fe650aee267388886aa6e4b,4000000,5000,1,669137, tx_missing_from_blockchain_for_2_days", - "10f32fe53081466f003185a9ef0324d6cbe3f59334ee9ccb2f7155cbfad9c1de": "kmbyoexc,10f32fe53081466f003185a9ef0324d6cbe3f59334ee9ccb2f7155cbfad9c1de,33000000,332,0,668954, tx_not_found", "cd99836ac4246c3e3980edf95773060481ce52271b74dadeb41e18c42ed21188": "nlaIlAvE,cd99836ac4246c3e3980edf95773060481ce52271b74dadeb41e18c42ed21188,5000000,546,1,669262, invalid_missing_fee_address", "fc3cb16293895fea8ea5d2d8ab4e39d1b27f583e2c160468b586789a861efa74": "feescammer,fc3cb16293895fea8ea5d2d8ab4e39d1b27f583e2c160468b586789a861efa74,1000000,546,1,669442, invalid_missing_fee_address", "72cabb5c323c923b43c7f6551974f591dcee148778ee34f9131011ea0ca82813": "PBFICEAS,72cabb5c323c923b43c7f6551974f591dcee148778ee34f9131011ea0ca82813,2000000,546,1,672969, dust_fee_scammer", diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java index 6237fa1450..176dd14c4f 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java @@ -211,26 +211,29 @@ public class PendingTradesViewModel extends ActivatableWithDataModel { - mempoolStatus.setValue(txValidator.isFail() ? 0 : 1); - if (txValidator.isFail()) { - String errorMessage = "Validation of Taker Tx returned: " + txValidator.toString(); - log.warn(errorMessage); - // prompt user to open mediation - if (trade.getDisputeState() == Trade.DisputeState.NO_DISPUTE) { - UserThread.runAfter(() -> { - Popup popup = new Popup(); - popup.headLine(Res.get("portfolio.pending.openSupportTicket.headline")) - .message(Res.get("portfolio.pending.invalidTx", errorMessage)) - .actionButtonText(Res.get("portfolio.pending.openSupportTicket.headline")) - .onAction(dataModel::onOpenSupportTicket) - .closeButtonText(Res.get("shared.cancel")) - .onClose(popup::hide) - .show(); - }, 100, TimeUnit.MILLISECONDS); + UserThread.runAfter(() -> { + mempoolService.validateOfferTakerTx(trade, (txValidator -> { + mempoolStatus.setValue(txValidator.isFail() ? 0 : 1); + if (txValidator.isFail()) { + String errorMessage = "Validation of Taker Tx returned: " + txValidator.toString(); + log.warn(errorMessage); + // prompt user to open mediation + if (trade.getDisputeState() == Trade.DisputeState.NO_DISPUTE) { + UserThread.runAfter(() -> { + Popup popup = new Popup(); + popup.headLine(Res.get("portfolio.pending.openSupportTicket.headline")) + .message(Res.get("portfolio.pending.invalidTx", errorMessage)) + .actionButtonText(Res.get("portfolio.pending.openSupportTicket.headline")) + .onAction(dataModel::onOpenSupportTicket) + .closeButtonText(Res.get("shared.cancel")) + .onClose(popup::hide) + .show(); + }, 100, TimeUnit.MILLISECONDS); + } } - } - })); + })); + }, Math.max(5000 - trade.getTradeAge(), 100), TimeUnit.MILLISECONDS); + // we wait until the trade has confirmed for at least 5 seconds to allow for DAO to process the block } /////////////////////////////////////////////////////////////////////////////////////////// From f8f920f1c69d52a4953e58fb4d8eab79c4f674ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Dec 2022 14:03:54 +0000 Subject: [PATCH 02/89] Bump actions/setup-java Bumps [actions/setup-java](https://github.com/actions/setup-java) from 19eeec562b37d29a1ad055b7de9c280bd0906d8d to c3ac5dd0ed8db40fedb61c32fbe677e6b355e94c. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/19eeec562b37d29a1ad055b7de9c280bd0906d8d...c3ac5dd0ed8db40fedb61c32fbe677e6b355e94c) --- updated-dependencies: - dependency-name: actions/setup-java dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 474ecf1c59..4f129093d5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Set up JDK - uses: actions/setup-java@19eeec562b37d29a1ad055b7de9c280bd0906d8d + uses: actions/setup-java@c3ac5dd0ed8db40fedb61c32fbe677e6b355e94c with: java-version: ${{ matrix.java }} distribution: 'zulu' From 5d30d44b15796b4f35db56249df9d3438e24e734 Mon Sep 17 00:00:00 2001 From: Alva Swanson Date: Wed, 14 Dec 2022 16:38:35 +0100 Subject: [PATCH 03/89] Gradle: Move protoc build-logic into its own build.gradle --- build.gradle | 41 ----------------------------------------- proto/build.gradle | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 41 deletions(-) create mode 100644 proto/build.gradle diff --git a/build.gradle b/build.gradle index 0810e1c7c5..8d6589243a 100644 --- a/build.gradle +++ b/build.gradle @@ -128,47 +128,6 @@ configure([project(':cli'), } } -configure(project(':proto')) { - apply plugin: 'com.google.protobuf' - - dependencies { - annotationProcessor libs.lombok - compileOnly libs.javax.annotation - compileOnly libs.lombok - implementation libs.logback.classic - implementation libs.logback.core - implementation libs.google.guava - implementation libs.protobuf.java - implementation libs.slf4j.api - implementation(libs.grpc.protobuf) { - exclude(module: 'animal-sniffer-annotations') - exclude(module: 'guava') - } - implementation(libs.grpc.stub) { - exclude(module: 'animal-sniffer-annotations') - exclude(module: 'guava') - } - } - - sourceSets.main.java.srcDirs += [ - 'build/generated/source/proto/main/grpc', - 'build/generated/source/proto/main/java' - ] - - protobuf { - protoc { - artifact = "com.google.protobuf:protoc:${protocVersion}" - } - plugins { - grpc { - artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" - } - } - generateProtoTasks { - all()*.plugins { grpc {} } - } - } -} configure(project(':assets')) { dependencies { diff --git a/proto/build.gradle b/proto/build.gradle new file mode 100644 index 0000000000..4336f76d77 --- /dev/null +++ b/proto/build.gradle @@ -0,0 +1,39 @@ +apply plugin: 'com.google.protobuf' + +dependencies { + annotationProcessor libs.lombok + compileOnly libs.javax.annotation + compileOnly libs.lombok + implementation libs.logback.classic + implementation libs.logback.core + implementation libs.google.guava + implementation libs.protobuf.java + implementation libs.slf4j.api + implementation(libs.grpc.protobuf) { + exclude(module: 'animal-sniffer-annotations') + exclude(module: 'guava') + } + implementation(libs.grpc.stub) { + exclude(module: 'animal-sniffer-annotations') + exclude(module: 'guava') + } +} + +sourceSets.main.java.srcDirs += [ + 'build/generated/source/proto/main/grpc', + 'build/generated/source/proto/main/java' +] + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${protocVersion}" + } + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" + } + } + generateProtoTasks { + all()*.plugins { grpc {} } + } +} From 2535f01eb1b55db992c9a7122791de11432d4e34 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 14 Dec 2022 18:50:41 -0500 Subject: [PATCH 04/89] Add check for price==0 Signed-off-by: HenrikJannsen --- .../desktop/main/dao/burnbsq/burningman/BalanceEntryItem.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BalanceEntryItem.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BalanceEntryItem.java index 84b0eba5eb..96640d7dbd 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BalanceEntryItem.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BalanceEntryItem.java @@ -104,7 +104,7 @@ class BalanceEntryItem { type = Optional.of(baseBalanceEntry.getType()); } - if (price.isEmpty() || receivedBtc.isEmpty()) { + if (price.isEmpty() || price.get().getValue() == 0 || receivedBtc.isEmpty()) { receivedBtcAsBsq = Optional.empty(); } else { long volume = price.get().getVolumeByAmount(Coin.valueOf(receivedBtc.get())).getValue(); From f799e115b0f5b87ae2739edbb99c6cf2dcabb576 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 14 Dec 2022 18:52:08 -0500 Subject: [PATCH 05/89] Remove try catch. Will be handled in Future fault handler Signed-off-by: HenrikJannsen --- .../bisq/network/p2p/network/NetworkNode.java | 155 +++++++++--------- 1 file changed, 75 insertions(+), 80 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java index 21a199ffa2..f5c724ac2b 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java +++ b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java @@ -130,7 +130,7 @@ public abstract class NetworkNode implements MessageListener { log.debug("We have not found any connection for peerAddress {}.\n\t" + "We will create a new outbound connection.", peersNodeAddress); - final SettableFuture resultFuture = SettableFuture.create(); + SettableFuture resultFuture = SettableFuture.create(); ListenableFuture future = connectionExecutor.submit(() -> { Thread.currentThread().setName("NetworkNode:SendMessage-to-" + peersNodeAddress.getFullAddress()); @@ -139,93 +139,88 @@ public abstract class NetworkNode implements MessageListener { } OutboundConnection outboundConnection; - try { - // can take a while when using tor - long startTs = System.currentTimeMillis(); + // can take a while when using tor + long startTs = System.currentTimeMillis(); - log.debug("Start create socket to peersNodeAddress {}", peersNodeAddress.getFullAddress()); + log.debug("Start create socket to peersNodeAddress {}", peersNodeAddress.getFullAddress()); - Socket socket = createSocket(peersNodeAddress); - long duration = System.currentTimeMillis() - startTs; - log.info("Socket creation to peersNodeAddress {} took {} ms", peersNodeAddress.getFullAddress(), - duration); + Socket socket = createSocket(peersNodeAddress); + long duration = System.currentTimeMillis() - startTs; + log.info("Socket creation to peersNodeAddress {} took {} ms", peersNodeAddress.getFullAddress(), + duration); - if (duration > CREATE_SOCKET_TIMEOUT) - throw new TimeoutException("A timeout occurred when creating a socket."); + if (duration > CREATE_SOCKET_TIMEOUT) + throw new TimeoutException("A timeout occurred when creating a socket."); - // Tor needs sometimes quite long to create a connection. To avoid that we get too many - // connections with the same peer we check again if we still don't have any connection for that node address. - Connection existingConnection = getInboundConnection(peersNodeAddress); - if (existingConnection == null) - existingConnection = getOutboundConnection(peersNodeAddress); + // Tor needs sometimes quite long to create a connection. To avoid that we get too many + // connections with the same peer we check again if we still don't have any connection for that node address. + Connection existingConnection = getInboundConnection(peersNodeAddress); + if (existingConnection == null) + existingConnection = getOutboundConnection(peersNodeAddress); - if (existingConnection != null) { - log.debug("We found in the meantime a connection for peersNodeAddress {}, " + - "so we use that for sending the message.\n" + - "That can happen if Tor needs long for creating a new outbound connection.\n" + - "We might have got a new inbound or outbound connection.", - peersNodeAddress.getFullAddress()); + if (existingConnection != null) { + log.debug("We found in the meantime a connection for peersNodeAddress {}, " + + "so we use that for sending the message.\n" + + "That can happen if Tor needs long for creating a new outbound connection.\n" + + "We might have got a new inbound or outbound connection.", + peersNodeAddress.getFullAddress()); - try { - socket.close(); - } catch (Throwable throwable) { - if (!shutDownInProgress) { - log.error("Error at closing socket " + throwable); - } + try { + socket.close(); + } catch (Throwable throwable) { + if (!shutDownInProgress) { + log.error("Error at closing socket " + throwable); } - existingConnection.sendMessage(networkEnvelope); - return existingConnection; - } else { - ConnectionListener connectionListener = new ConnectionListener() { - @Override - public void onConnection(Connection connection) { - if (!connection.isStopped()) { - outBoundConnections.add((OutboundConnection) connection); - printOutBoundConnections(); - connectionListeners.forEach(e -> e.onConnection(connection)); - } - } - - @Override - public void onDisconnect(CloseConnectionReason closeConnectionReason, - Connection connection) { - //noinspection SuspiciousMethodCalls - outBoundConnections.remove(connection); - printOutBoundConnections(); - connectionListeners.forEach(e -> e.onDisconnect(closeConnectionReason, connection)); - } - - @Override - public void onError(Throwable throwable) { - if (!shutDownInProgress) { - log.error("new OutboundConnection.ConnectionListener.onError " + throwable.getMessage()); - } - connectionListeners.forEach(e -> e.onError(throwable)); - } - }; - outboundConnection = new OutboundConnection(socket, - NetworkNode.this, - connectionListener, - peersNodeAddress, - networkProtoResolver, - networkFilter); - - if (log.isDebugEnabled()) { - log.debug("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" + - "NetworkNode created new outbound connection:" - + "\nmyNodeAddress=" + getNodeAddress() - + "\npeersNodeAddress=" + peersNodeAddress - + "\nuid=" + outboundConnection.getUid() - + "\nmessage=" + networkEnvelope - + "\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n"); - } - // can take a while when using tor - outboundConnection.sendMessage(networkEnvelope); - return outboundConnection; } - } catch (IOException | TimeoutException throwable) { - log.warn("Executing task failed. " + throwable.getMessage()); - throw throwable; + existingConnection.sendMessage(networkEnvelope); + return existingConnection; + } else { + ConnectionListener connectionListener = new ConnectionListener() { + @Override + public void onConnection(Connection connection) { + if (!connection.isStopped()) { + outBoundConnections.add((OutboundConnection) connection); + printOutBoundConnections(); + connectionListeners.forEach(e -> e.onConnection(connection)); + } + } + + @Override + public void onDisconnect(CloseConnectionReason closeConnectionReason, + Connection connection) { + //noinspection SuspiciousMethodCalls + outBoundConnections.remove(connection); + printOutBoundConnections(); + connectionListeners.forEach(e -> e.onDisconnect(closeConnectionReason, connection)); + } + + @Override + public void onError(Throwable throwable) { + if (!shutDownInProgress) { + log.error("new OutboundConnection.ConnectionListener.onError " + throwable.getMessage()); + } + connectionListeners.forEach(e -> e.onError(throwable)); + } + }; + outboundConnection = new OutboundConnection(socket, + NetworkNode.this, + connectionListener, + peersNodeAddress, + networkProtoResolver, + networkFilter); + + if (log.isDebugEnabled()) { + log.debug("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" + + "NetworkNode created new outbound connection:" + + "\nmyNodeAddress=" + getNodeAddress() + + "\npeersNodeAddress=" + peersNodeAddress + + "\nuid=" + outboundConnection.getUid() + + "\nmessage=" + networkEnvelope + + "\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n"); + } + // can take a while when using tor + outboundConnection.sendMessage(networkEnvelope); + return outboundConnection; } }); From 4860c1177a70c0cadf4370c07c1a7932e2757575 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 14 Dec 2022 20:34:42 -0500 Subject: [PATCH 06/89] Change burn target calculation. Left side is amount to burn to reach the max allowed receiver share based on the burned amount of all BM. The right side is the amount to burn to reach the max allowed receiver share based the boosted max burn target. Increase ISSUANCE_BOOST_FACTOR from 3 to 4. Add help overlay to burn target table header. Signed-off-by: HenrikJannsen --- .../BurningManPresentationService.java | 38 +++++++++---------- .../dao/burningman/BurningManService.java | 6 +-- .../burningman/model/BurningManCandidate.java | 7 +++- .../resources/i18n/displayStrings.properties | 4 ++ .../burnbsq/burningman/BurningManView.java | 3 +- 5 files changed, 31 insertions(+), 27 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java b/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java index d019eb73c4..5ba1cd3e64 100644 --- a/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java +++ b/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java @@ -61,10 +61,7 @@ import lombok.extern.slf4j.Slf4j; public class BurningManPresentationService implements DaoStateListener { // Burn target gets increased by that amount to give more flexibility. // Burn target is calculated from reimbursements + estimated BTC fees - burned amounts. - private static final long BURN_TARGET_BOOST_AMOUNT = Config.baseCurrencyNetwork().isRegtest() ? 1000000 : 10000000; - // To avoid that the BM get locked in small total burn amounts we allow to burn up to 1000 BSQ more than the - // calculation to not exceed the cap would suggest. - private static final long MAX_BURN_TARGET_LOWER_FLOOR = 100000; + private static final long BURN_TARGET_BOOST_AMOUNT = 10000000; public static final String LEGACY_BURNING_MAN_DPT_NAME = "Legacy Burningman (DPT)"; public static final String LEGACY_BURNING_MAN_BTC_FEES_NAME = "Legacy Burningman (BTC fees)"; static final String LEGACY_BURNING_MAN_BTC_FEES_ADDRESS = "38bZBj5peYS3Husdz7AH3gEUiUbYRD951t"; @@ -173,44 +170,43 @@ public class BurningManPresentationService implements DaoStateListener { return Math.round(burningManCandidate.getCappedBurnAmountShare() * getAverageDistributionPerCycle()); } + // Left side in tuple is the amount to burn to reach the max. burn share based on the total burned amount. + // This value is safe to not burn more than needed and to avoid to get capped. + // The right side is the amount to burn to reach the max. burn share based on the boosted burn target. + // This can lead to burning too much and getting capped. public Tuple2 getCandidateBurnTarget(BurningManCandidate burningManCandidate) { long burnTarget = getBurnTarget(); + long boostedBurnTarget = burnTarget + BURN_TARGET_BOOST_AMOUNT; double compensationShare = burningManCandidate.getCompensationShare(); - if (burnTarget == 0 || compensationShare == 0) { + + if (boostedBurnTarget <= 0 || compensationShare == 0) { return new Tuple2<>(0L, 0L); } double maxCompensationShare = Math.min(BurningManService.MAX_BURN_SHARE, compensationShare); long lowerBaseTarget = Math.round(burnTarget * maxCompensationShare); - long boostedBurnAmount = burnTarget + BURN_TARGET_BOOST_AMOUNT; - double maxBoostedCompensationShare = Math.min(BurningManService.MAX_BURN_SHARE, compensationShare * BurningManService.ISSUANCE_BOOST_FACTOR); - long upperBaseTarget = Math.round(boostedBurnAmount * maxBoostedCompensationShare); + double maxBoostedCompensationShare = burningManCandidate.getMaxBoostedCompensationShare(); + long upperBaseTarget = Math.round(boostedBurnTarget * maxBoostedCompensationShare); long totalBurnedAmount = burnTargetService.getAccumulatedDecayedBurnedAmount(getBurningManCandidatesByName().values(), currentChainHeight); if (totalBurnedAmount == 0) { + // The first BM would reach their max burn share by 5.46 BSQ already. But we suggest the lowerBaseTarget + // as lower target to speed up the bootstrapping. return new Tuple2<>(lowerBaseTarget, upperBaseTarget); } double burnAmountShare = burningManCandidate.getBurnAmountShare(); - long candidatesBurnAmount = burningManCandidate.getAccumulatedDecayedBurnAmount(); if (burnAmountShare < maxBoostedCompensationShare) { - long myBurnAmount = getMissingAmountToReachTargetShare(totalBurnedAmount, candidatesBurnAmount, maxCompensationShare); - long myMaxBurnAmount = getMissingAmountToReachTargetShare(totalBurnedAmount, candidatesBurnAmount, maxBoostedCompensationShare); - - // We limit to base targets - myBurnAmount = Math.min(myBurnAmount, lowerBaseTarget); - myMaxBurnAmount = Math.min(myMaxBurnAmount, upperBaseTarget); - - // We allow at least MAX_BURN_TARGET_LOWER_FLOOR (1000 BSQ) to burn, even if that means to hit the cap to give more flexibility - // when low amounts are burned and the 11% cap would lock in BM to small increments per burn iteration. - myMaxBurnAmount = Math.max(myMaxBurnAmount, MAX_BURN_TARGET_LOWER_FLOOR); + long candidatesBurnAmount = burningManCandidate.getAccumulatedDecayedBurnAmount(); + long myBurnAmount = getMissingAmountToReachTargetShare(totalBurnedAmount, candidatesBurnAmount, maxBoostedCompensationShare); // If below dust we set value to 0 myBurnAmount = myBurnAmount < 546 ? 0 : myBurnAmount; - return new Tuple2<>(myBurnAmount, myMaxBurnAmount); + + return new Tuple2<>(myBurnAmount, upperBaseTarget); } else { // We have reached our cap. - return new Tuple2<>(0L, MAX_BURN_TARGET_LOWER_FLOOR); + return new Tuple2<>(0L, upperBaseTarget); } } diff --git a/core/src/main/java/bisq/core/dao/burningman/BurningManService.java b/core/src/main/java/bisq/core/dao/burningman/BurningManService.java index 306a388bad..b2247798a2 100644 --- a/core/src/main/java/bisq/core/dao/burningman/BurningManService.java +++ b/core/src/main/java/bisq/core/dao/burningman/BurningManService.java @@ -91,9 +91,9 @@ public class BurningManService { // Factor for boosting the issuance share (issuance is compensation requests + genesis output). // This will be used for increasing the allowed burn amount. The factor gives more flexibility // and compensates for those who do not burn. The burn share is capped by that factor as well. - // E.g. a contributor with 10% issuance share will be able to receive max 20% of the BTC fees or DPT output - // even if they had burned more and had a higher burn share than 20%. - public static final double ISSUANCE_BOOST_FACTOR = 3; + // E.g. a contributor with 2% issuance share will be able to receive max 8% of the BTC fees or DPT output + // even if they had burned more and had a higher burn share than 8%. + public static final double ISSUANCE_BOOST_FACTOR = 4; // The max amount the burn share can reach. This value is derived from the min. security deposit in a trade and // ensures that an attack where a BM would take all sell offers cannot be economically profitable as they would diff --git a/core/src/main/java/bisq/core/dao/burningman/model/BurningManCandidate.java b/core/src/main/java/bisq/core/dao/burningman/model/BurningManCandidate.java index 32f35e91bb..0f6152e70a 100644 --- a/core/src/main/java/bisq/core/dao/burningman/model/BurningManCandidate.java +++ b/core/src/main/java/bisq/core/dao/burningman/model/BurningManCandidate.java @@ -93,10 +93,13 @@ public class BurningManCandidate { public void calculateShares(double totalDecayedCompensationAmounts, double totalDecayedBurnAmounts) { compensationShare = totalDecayedCompensationAmounts > 0 ? accumulatedDecayedCompensationAmount / totalDecayedCompensationAmounts : 0; - double maxBoostedCompensationShare = Math.min(BurningManService.MAX_BURN_SHARE, compensationShare * BurningManService.ISSUANCE_BOOST_FACTOR); burnAmountShare = totalDecayedBurnAmounts > 0 ? accumulatedDecayedBurnAmount / totalDecayedBurnAmounts : 0; - cappedBurnAmountShare = Math.min(maxBoostedCompensationShare, burnAmountShare); + cappedBurnAmountShare = Math.min(getMaxBoostedCompensationShare(), burnAmountShare); + } + + public double getMaxBoostedCompensationShare() { + return Math.min(BurningManService.MAX_BURN_SHARE, compensationShare * BurningManService.ISSUANCE_BOOST_FACTOR); } @Override diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index ffbb9e3912..6c93723798 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2307,6 +2307,10 @@ dao.burningman.shared.table.cycle=Cycle dao.burningman.shared.table.date=Date dao.burningman.table.name=Contributor name dao.burningman.table.burnTarget=Burn target +dao.burningman.table.burnTarget.help=The left value is the amount to burn to reach the max. receiver share based on the total burned amount.\n\ + This value is safe to not burn more than needed and to avoid to get a capped receiver share.\n\n\ + The right value is the amount to burn to reach the max. receiver share based on the max. burn target.\n\ + This can lead to a capped receiver share but once the burn target is reached it should fall below the cap. dao.burningman.table.expectedRevenue=Expected to receive dao.burningman.table.burnAmount=Burned amount dao.burningman.table.decayedBurnAmount=Decayed burned amount diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManView.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManView.java index d0752e3abd..252fd97a8b 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManView.java @@ -756,8 +756,9 @@ public class BurningManView extends ActivatableView implements burningManTableView.getColumns().add(column); column.setComparator(Comparator.comparing(e -> e.getName().toLowerCase())); - column = new AutoTooltipTableColumn<>(Res.get("dao.burningman.table.burnTarget")); + column = new AutoTooltipTableColumn<>(Res.get("dao.burningman.table.burnTarget"), Res.get("dao.burningman.table.burnTarget.help")); column.setMinWidth(200); + column.getGraphic().setStyle("-fx-alignment: center-right; -fx-padding: 2 10 2 0"); column.getStyleClass().add("last-column"); column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); column.setCellFactory(new Callback<>() { From d0539e17ef83918cfec2491b2409824bf75744a9 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 14 Dec 2022 21:44:54 -0500 Subject: [PATCH 07/89] In case we have capped burn shares we redistribute the share from the over-burned amount to the non capped candidates. This helps to avoid that the legacy BM would get the rest in case there are capped shares. It still can be that a candidate exceeds the cap and by the adjustment becomes capped. We take that into account and the legacy BM would get some share in that case. Signed-off-by: HenrikJannsen --- .../dao/burningman/BurningManService.java | 11 +++++ .../burningman/model/BurningManCandidate.java | 45 +++++++++++++++++-- .../burningman/BurningManListItem.java | 12 ++--- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/BurningManService.java b/core/src/main/java/bisq/core/dao/burningman/BurningManService.java index b2247798a2..43989b21ea 100644 --- a/core/src/main/java/bisq/core/dao/burningman/BurningManService.java +++ b/core/src/main/java/bisq/core/dao/burningman/BurningManService.java @@ -189,6 +189,17 @@ public class BurningManService { .mapToDouble(BurningManCandidate::getAccumulatedDecayedBurnAmount) .sum(); burningManCandidates.forEach(candidate -> candidate.calculateShares(totalDecayedCompensationAmounts, totalDecayedBurnAmounts)); + + double sumAllCappedBurnAmountShares = burningManCandidates.stream() + .filter(candidate -> candidate.getBurnAmountShare() >= candidate.getMaxBoostedCompensationShare()) + .mapToDouble(BurningManCandidate::getMaxBoostedCompensationShare) + .sum(); + double sumAllNonCappedBurnAmountShares = burningManCandidates.stream() + .filter(candidate -> candidate.getBurnAmountShare() < candidate.getMaxBoostedCompensationShare()) + .mapToDouble(BurningManCandidate::getBurnAmountShare) + .sum(); + burningManCandidates.forEach(candidate -> candidate.calculateCappedBurnAmountShare(sumAllCappedBurnAmountShares, sumAllNonCappedBurnAmountShares)); + return burningManCandidatesByName; } diff --git a/core/src/main/java/bisq/core/dao/burningman/model/BurningManCandidate.java b/core/src/main/java/bisq/core/dao/burningman/model/BurningManCandidate.java index 0f6152e70a..39746fa66a 100644 --- a/core/src/main/java/bisq/core/dao/burningman/model/BurningManCandidate.java +++ b/core/src/main/java/bisq/core/dao/burningman/model/BurningManCandidate.java @@ -52,8 +52,13 @@ public class BurningManCandidate { private final Map> burnOutputModelsByMonth = new HashMap<>(); private long accumulatedBurnAmount; private long accumulatedDecayedBurnAmount; - protected double burnAmountShare; // Share of accumulated decayed burn amounts in relation to total burned amounts - protected double cappedBurnAmountShare; // Capped burnAmountShare. Cannot be larger than boostedCompensationShare + // Share of accumulated decayed burn amounts in relation to total burned amounts + protected double burnAmountShare; + // Capped burnAmountShare. Cannot be larger than boostedCompensationShare + protected double cappedBurnAmountShare; + // The burnAmountShare adjusted in case there are cappedBurnAmountShare. + // We redistribute the over-burned amounts to the group of not capped candidates. + private double adjustedBurnAmountShare; public BurningManCandidate() { } @@ -93,11 +98,42 @@ public class BurningManCandidate { public void calculateShares(double totalDecayedCompensationAmounts, double totalDecayedBurnAmounts) { compensationShare = totalDecayedCompensationAmounts > 0 ? accumulatedDecayedCompensationAmount / totalDecayedCompensationAmounts : 0; - burnAmountShare = totalDecayedBurnAmounts > 0 ? accumulatedDecayedBurnAmount / totalDecayedBurnAmounts : 0; - cappedBurnAmountShare = Math.min(getMaxBoostedCompensationShare(), burnAmountShare); } + public void calculateCappedBurnAmountShare(double sumAllCappedBurnAmountShares, + double sumAllNonCappedBurnAmountShares) { + double maxBoostedCompensationShare = getMaxBoostedCompensationShare(); + adjustedBurnAmountShare = burnAmountShare; + if (burnAmountShare < maxBoostedCompensationShare) { + if (sumAllCappedBurnAmountShares == 0) { + // If no one is capped we do not need to do any adjustment + cappedBurnAmountShare = burnAmountShare; + } else { + // The difference of the cappedBurnAmountShare and burnAmountShare will get redistributed to all + // non-capped candidates. + double distributionBase = 1 - sumAllCappedBurnAmountShares; + if (sumAllNonCappedBurnAmountShares == 0) { + // In case we get sumAllNonCappedBurnAmountShares our burnAmountShare is also 0. + cappedBurnAmountShare = burnAmountShare; + } else { + double adjustment = distributionBase / sumAllNonCappedBurnAmountShares; + adjustedBurnAmountShare = burnAmountShare * adjustment; + if (adjustedBurnAmountShare < maxBoostedCompensationShare) { + cappedBurnAmountShare = adjustedBurnAmountShare; + } else { + // We exceeded the cap by the adjustment. This will lead to the legacy BM getting the + // difference of the adjusted amount and the maxBoostedCompensationShare. + cappedBurnAmountShare = maxBoostedCompensationShare; + } + } + } + } else { + cappedBurnAmountShare = maxBoostedCompensationShare; + } + } + + public double getMaxBoostedCompensationShare() { return Math.min(BurningManService.MAX_BURN_SHARE, compensationShare * BurningManService.ISSUANCE_BOOST_FACTOR); } @@ -115,6 +151,7 @@ public class BurningManCandidate { ",\r\n accumulatedDecayedBurnAmount=" + accumulatedDecayedBurnAmount + ",\r\n burnAmountShare=" + burnAmountShare + ",\r\n cappedBurnAmountShare=" + cappedBurnAmountShare + + ",\r\n adjustedBurnAmountShare=" + adjustedBurnAmountShare + "\r\n}"; } } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManListItem.java index aad6dd6d5c..ada8528c6e 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManListItem.java @@ -39,7 +39,7 @@ class BurningManListItem { private final long burnTarget, maxBurnTarget, accumulatedDecayedBurnAmount, accumulatedBurnAmount, accumulatedDecayedCompensationAmount, accumulatedCompensationAmount, expectedRevenue; private final int numBurnOutputs, numIssuances; - private final double cappedBurnAmountShare, burnAmountShare, compensationShare; + private final double cappedBurnAmountShare, adjustedBurnAmountShare, compensationShare; BurningManListItem(BurningManPresentationService burningManPresentationService, String name, @@ -60,14 +60,14 @@ class BurningManListItem { accumulatedDecayedBurnAmount = 0; accumulatedDecayedBurnAmountAsBsq = ""; - burnAmountShare = burningManCandidate.getBurnAmountShare(); + adjustedBurnAmountShare = burningManCandidate.getAdjustedBurnAmountShare(); cappedBurnAmountShare = burningManCandidate.getCappedBurnAmountShare(); // LegacyBurningManForDPT is the one defined by DAO voting, so only that would receive BTC if new BM do not cover 100%. if (burningManPresentationService.getLegacyBurningManForDPT().equals(burningManCandidate)) { expectedRevenue = burningManPresentationService.getExpectedRevenue(burningManCandidate); - cappedBurnAmountShareAsString = FormattingUtils.formatToPercentWithSymbol(burnAmountShare); + cappedBurnAmountShareAsString = FormattingUtils.formatToPercentWithSymbol(adjustedBurnAmountShare); } else { expectedRevenue = 0; cappedBurnAmountShareAsString = FormattingUtils.formatToPercentWithSymbol(0); @@ -94,12 +94,12 @@ class BurningManListItem { accumulatedBurnAmountAsBsq = bsqFormatter.formatCoinWithCode(accumulatedBurnAmount); accumulatedDecayedBurnAmount = burningManCandidate.getAccumulatedDecayedBurnAmount(); accumulatedDecayedBurnAmountAsBsq = bsqFormatter.formatCoinWithCode(accumulatedDecayedBurnAmount); - burnAmountShare = burningManCandidate.getBurnAmountShare(); + adjustedBurnAmountShare = burningManCandidate.getAdjustedBurnAmountShare(); cappedBurnAmountShare = burningManCandidate.getCappedBurnAmountShare(); - if (burnAmountShare != cappedBurnAmountShare) { + if (adjustedBurnAmountShare != cappedBurnAmountShare) { cappedBurnAmountShareAsString = Res.get("dao.burningman.table.burnAmountShare.capped", FormattingUtils.formatToPercentWithSymbol(cappedBurnAmountShare), - FormattingUtils.formatToPercentWithSymbol(burnAmountShare)); + FormattingUtils.formatToPercentWithSymbol(adjustedBurnAmountShare)); } else { cappedBurnAmountShareAsString = FormattingUtils.formatToPercentWithSymbol(cappedBurnAmountShare); } From 7f73ef7cb5f120de39cb83dc3f41ee24fd963d8e Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 14 Dec 2022 21:48:58 -0500 Subject: [PATCH 08/89] Don't allow the myBurnAmount to be larger than the upperBaseTarget Signed-off-by: HenrikJannsen --- .../core/dao/burningman/BurningManPresentationService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java b/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java index 5ba1cd3e64..ab10d86089 100644 --- a/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java +++ b/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java @@ -203,6 +203,9 @@ public class BurningManPresentationService implements DaoStateListener { // If below dust we set value to 0 myBurnAmount = myBurnAmount < 546 ? 0 : myBurnAmount; + // In case the myBurnAmount would be larger than the upperBaseTarget we use the upperBaseTarget. + myBurnAmount = Math.min(myBurnAmount, upperBaseTarget); + return new Tuple2<>(myBurnAmount, upperBaseTarget); } else { // We have reached our cap. From d58545673276699265570d965a9e7931e9a97f4e Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 15 Dec 2022 11:18:54 -0500 Subject: [PATCH 09/89] Refactor: move out fields used the same way in both if/else branches. Signed-off-by: HenrikJannsen --- .../burningman/BurningManListItem.java | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManListItem.java index ada8528c6e..031fe65be5 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManListItem.java @@ -50,30 +50,29 @@ class BurningManListItem { this.name = name; address = burningManCandidate.getMostRecentAddress().orElse(Res.get("shared.na")); + adjustedBurnAmountShare = burningManCandidate.getAdjustedBurnAmountShare(); + cappedBurnAmountShare = burningManCandidate.getCappedBurnAmountShare(); + accumulatedBurnAmount = burningManCandidate.getAccumulatedBurnAmount(); + accumulatedBurnAmountAsBsq = bsqFormatter.formatCoinWithCode(accumulatedBurnAmount); + numBurnOutputs = burningManCandidate.getBurnOutputModels().size(); + if (burningManCandidate instanceof LegacyBurningMan) { // Burn burnTarget = 0; - burnTargetAsBsq = ""; maxBurnTarget = 0; - accumulatedBurnAmount = burningManCandidate.getAccumulatedBurnAmount(); - accumulatedBurnAmountAsBsq = bsqFormatter.formatCoinWithCode(accumulatedBurnAmount); + burnTargetAsBsq = ""; + accumulatedDecayedBurnAmount = 0; accumulatedDecayedBurnAmountAsBsq = ""; - adjustedBurnAmountShare = burningManCandidate.getAdjustedBurnAmountShare(); - - cappedBurnAmountShare = burningManCandidate.getCappedBurnAmountShare(); - // LegacyBurningManForDPT is the one defined by DAO voting, so only that would receive BTC if new BM do not cover 100%. if (burningManPresentationService.getLegacyBurningManForDPT().equals(burningManCandidate)) { - expectedRevenue = burningManPresentationService.getExpectedRevenue(burningManCandidate); cappedBurnAmountShareAsString = FormattingUtils.formatToPercentWithSymbol(adjustedBurnAmountShare); + expectedRevenue = burningManPresentationService.getExpectedRevenue(burningManCandidate); } else { - expectedRevenue = 0; cappedBurnAmountShareAsString = FormattingUtils.formatToPercentWithSymbol(0); + expectedRevenue = 0; } - expectedRevenueAsBsq = bsqFormatter.formatCoinWithCode(expectedRevenue); - numBurnOutputs = burningManCandidate.getBurnOutputModels().size(); // There is no issuance for legacy BM accumulatedCompensationAmount = 0; @@ -90,12 +89,9 @@ class BurningManListItem { burnTarget = burnTargetTuple.first; maxBurnTarget = burnTargetTuple.second; burnTargetAsBsq = Res.get("dao.burningman.burnTarget.fromTo", bsqFormatter.formatCoin(burnTarget), bsqFormatter.formatCoin(maxBurnTarget)); - accumulatedBurnAmount = burningManCandidate.getAccumulatedBurnAmount(); - accumulatedBurnAmountAsBsq = bsqFormatter.formatCoinWithCode(accumulatedBurnAmount); + accumulatedDecayedBurnAmount = burningManCandidate.getAccumulatedDecayedBurnAmount(); accumulatedDecayedBurnAmountAsBsq = bsqFormatter.formatCoinWithCode(accumulatedDecayedBurnAmount); - adjustedBurnAmountShare = burningManCandidate.getAdjustedBurnAmountShare(); - cappedBurnAmountShare = burningManCandidate.getCappedBurnAmountShare(); if (adjustedBurnAmountShare != cappedBurnAmountShare) { cappedBurnAmountShareAsString = Res.get("dao.burningman.table.burnAmountShare.capped", FormattingUtils.formatToPercentWithSymbol(cappedBurnAmountShare), @@ -104,8 +100,6 @@ class BurningManListItem { cappedBurnAmountShareAsString = FormattingUtils.formatToPercentWithSymbol(cappedBurnAmountShare); } expectedRevenue = burningManPresentationService.getExpectedRevenue(burningManCandidate); - expectedRevenueAsBsq = bsqFormatter.formatCoinWithCode(expectedRevenue); - numBurnOutputs = burningManCandidate.getBurnOutputModels().size(); // Issuance accumulatedCompensationAmount = burningManCandidate.getAccumulatedCompensationAmount(); @@ -117,5 +111,8 @@ class BurningManListItem { numIssuances = burningManCandidate.getCompensationModels().size(); numIssuancesAsString = String.valueOf(numIssuances); } + + expectedRevenueAsBsq = bsqFormatter.formatCoinWithCode(expectedRevenue); + } } From 0a941c1719a77c5d000129662f9f6e1d700f94d7 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 15 Dec 2022 11:20:24 -0500 Subject: [PATCH 10/89] Add custom handling of legacy BM. Refactoring: rename variables Signed-off-by: HenrikJannsen --- .../burningman/BurningManPresentationService.java | 15 +++++++++------ .../core/dao/burningman/BurningManService.java | 2 +- .../dao/burningman/model/BurningManCandidate.java | 6 +++--- .../dao/burningman/model/LegacyBurningMan.java | 11 +++++++++++ 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java b/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java index ab10d86089..aa4398c60e 100644 --- a/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java +++ b/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java @@ -44,6 +44,7 @@ import javax.inject.Singleton; import com.google.common.annotations.VisibleForTesting; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -187,7 +188,8 @@ public class BurningManPresentationService implements DaoStateListener { long lowerBaseTarget = Math.round(burnTarget * maxCompensationShare); double maxBoostedCompensationShare = burningManCandidate.getMaxBoostedCompensationShare(); long upperBaseTarget = Math.round(boostedBurnTarget * maxBoostedCompensationShare); - long totalBurnedAmount = burnTargetService.getAccumulatedDecayedBurnedAmount(getBurningManCandidatesByName().values(), currentChainHeight); + Collection burningManCandidates = getBurningManCandidatesByName().values(); + long totalBurnedAmount = burnTargetService.getAccumulatedDecayedBurnedAmount(burningManCandidates, currentChainHeight); if (totalBurnedAmount == 0) { // The first BM would reach their max burn share by 5.46 BSQ already. But we suggest the lowerBaseTarget @@ -195,9 +197,10 @@ public class BurningManPresentationService implements DaoStateListener { return new Tuple2<>(lowerBaseTarget, upperBaseTarget); } - double burnAmountShare = burningManCandidate.getBurnAmountShare(); - if (burnAmountShare < maxBoostedCompensationShare) { + if (burningManCandidate.getAdjustedBurnAmountShare() < maxBoostedCompensationShare) { long candidatesBurnAmount = burningManCandidate.getAccumulatedDecayedBurnAmount(); + + // TODO We do not consider adjustedBurnAmountShare. This could lead to slight over burn. Atm we ignore that. long myBurnAmount = getMissingAmountToReachTargetShare(totalBurnedAmount, candidatesBurnAmount, maxBoostedCompensationShare); // If below dust we set value to 0 @@ -214,11 +217,11 @@ public class BurningManPresentationService implements DaoStateListener { } @VisibleForTesting - static long getMissingAmountToReachTargetShare(long total, long myAmount, double myTargetShare) { - long others = total - myAmount; + static long getMissingAmountToReachTargetShare(long totalBurnedAmount, long myBurnAmount, double myTargetShare) { + long others = totalBurnedAmount - myBurnAmount; double shareTargetOthers = 1 - myTargetShare; double targetAmount = shareTargetOthers > 0 ? myTargetShare / shareTargetOthers * others : 0; - return Math.round(targetAmount) - myAmount; + return Math.round(targetAmount) - myBurnAmount; } public Set getReimbursements() { diff --git a/core/src/main/java/bisq/core/dao/burningman/BurningManService.java b/core/src/main/java/bisq/core/dao/burningman/BurningManService.java index 43989b21ea..8d21627e27 100644 --- a/core/src/main/java/bisq/core/dao/burningman/BurningManService.java +++ b/core/src/main/java/bisq/core/dao/burningman/BurningManService.java @@ -198,7 +198,7 @@ public class BurningManService { .filter(candidate -> candidate.getBurnAmountShare() < candidate.getMaxBoostedCompensationShare()) .mapToDouble(BurningManCandidate::getBurnAmountShare) .sum(); - burningManCandidates.forEach(candidate -> candidate.calculateCappedBurnAmountShare(sumAllCappedBurnAmountShares, sumAllNonCappedBurnAmountShares)); + burningManCandidates.forEach(candidate -> candidate.calculateCappedAndAdjustedShares(sumAllCappedBurnAmountShares, sumAllNonCappedBurnAmountShares)); return burningManCandidatesByName; } diff --git a/core/src/main/java/bisq/core/dao/burningman/model/BurningManCandidate.java b/core/src/main/java/bisq/core/dao/burningman/model/BurningManCandidate.java index 39746fa66a..83c8ef01af 100644 --- a/core/src/main/java/bisq/core/dao/burningman/model/BurningManCandidate.java +++ b/core/src/main/java/bisq/core/dao/burningman/model/BurningManCandidate.java @@ -58,7 +58,7 @@ public class BurningManCandidate { protected double cappedBurnAmountShare; // The burnAmountShare adjusted in case there are cappedBurnAmountShare. // We redistribute the over-burned amounts to the group of not capped candidates. - private double adjustedBurnAmountShare; + protected double adjustedBurnAmountShare; public BurningManCandidate() { } @@ -101,8 +101,8 @@ public class BurningManCandidate { burnAmountShare = totalDecayedBurnAmounts > 0 ? accumulatedDecayedBurnAmount / totalDecayedBurnAmounts : 0; } - public void calculateCappedBurnAmountShare(double sumAllCappedBurnAmountShares, - double sumAllNonCappedBurnAmountShares) { + public void calculateCappedAndAdjustedShares(double sumAllCappedBurnAmountShares, + double sumAllNonCappedBurnAmountShares) { double maxBoostedCompensationShare = getMaxBoostedCompensationShare(); adjustedBurnAmountShare = burnAmountShare; if (burnAmountShare < maxBoostedCompensationShare) { diff --git a/core/src/main/java/bisq/core/dao/burningman/model/LegacyBurningMan.java b/core/src/main/java/bisq/core/dao/burningman/model/LegacyBurningMan.java index 46244b362b..7098b9a079 100644 --- a/core/src/main/java/bisq/core/dao/burningman/model/LegacyBurningMan.java +++ b/core/src/main/java/bisq/core/dao/burningman/model/LegacyBurningMan.java @@ -35,6 +35,11 @@ public final class LegacyBurningMan extends BurningManCandidate { public void applyBurnAmountShare(double burnAmountShare) { this.burnAmountShare = burnAmountShare; + + // We do not adjust burnAmountShare for legacy BM from capped BM + this.adjustedBurnAmountShare = burnAmountShare; + + // We do not cap burnAmountShare for legacy BM this.cappedBurnAmountShare = burnAmountShare; } @@ -43,6 +48,12 @@ public final class LegacyBurningMan extends BurningManCandidate { // do nothing } + @Override + public void calculateCappedAndAdjustedShares(double sumAllCappedBurnAmountShares, + double sumAllNonCappedBurnAmountShares) { + // do nothing + } + @Override public Set getAllAddresses() { return mostRecentAddress.map(Set::of).orElse(new HashSet<>()); From 6fd68f76a9b0cebc166b999d4bde3724327e1075 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 15 Dec 2022 12:00:53 -0500 Subject: [PATCH 11/89] Remove regtest value for DEFAULT_ESTIMATED_BTC_TRADE_FEE_REVENUE_PER_CYCLE Adjust reimbursement amounts as we do not reimburse 100% of the DPT Signed-off-by: HenrikJannsen --- .../dao/burningman/BurnTargetService.java | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/BurnTargetService.java b/core/src/main/java/bisq/core/dao/burningman/BurnTargetService.java index c770d4ddda..6acc958759 100644 --- a/core/src/main/java/bisq/core/dao/burningman/BurnTargetService.java +++ b/core/src/main/java/bisq/core/dao/burningman/BurnTargetService.java @@ -56,10 +56,13 @@ class BurnTargetService { private static final int NUM_CYCLES_BURN_TARGET = 12; private static final int NUM_CYCLES_AVERAGE_DISTRIBUTION = 3; + // Estimated block at activation date + private static final int ACTIVATION_BLOCK = Config.baseCurrencyNetwork().isRegtest() ? 111 : 769845; + // Default value for the estimated BTC trade fees per month as BSQ sat value (100 sat = 1 BSQ). // Default is roughly average of last 12 months at Nov 2022. // Can be changed with DAO parameter voting. - private static final long DEFAULT_ESTIMATED_BTC_TRADE_FEE_REVENUE_PER_CYCLE = Config.baseCurrencyNetwork().isRegtest() ? 1000000 : 6200000; + private static final long DEFAULT_ESTIMATED_BTC_TRADE_FEE_REVENUE_PER_CYCLE = 6200000; private final DaoStateService daoStateService; private final CyclesInDaoStateService cyclesInDaoStateService; @@ -102,7 +105,7 @@ class BurnTargetService { long getBurnTarget(int chainHeight, Collection burningManCandidates) { // Reimbursements are taken into account at result vote block int chainHeightOfPastCycle = cyclesInDaoStateService.getChainHeightOfPastCycle(chainHeight, NUM_CYCLES_BURN_TARGET); - long accumulatedReimbursements = getAccumulatedReimbursements(chainHeight, chainHeightOfPastCycle); + long accumulatedReimbursements = getAdjustedAccumulatedReimbursements(chainHeight, chainHeightOfPastCycle); // Param changes are taken into account at first block at next cycle after voting int heightOfFirstBlockOfPastCycle = cyclesInDaoStateService.getHeightOfFirstBlockOfPastCycle(chainHeight, NUM_CYCLES_BURN_TARGET - 1); @@ -140,7 +143,7 @@ class BurnTargetService { long getAverageDistributionPerCycle(int chainHeight) { // Reimbursements are taken into account at result vote block int chainHeightOfPastCycle = cyclesInDaoStateService.getChainHeightOfPastCycle(chainHeight, NUM_CYCLES_AVERAGE_DISTRIBUTION); - long reimbursements = getAccumulatedReimbursements(chainHeight, chainHeightOfPastCycle); + long reimbursements = getAdjustedAccumulatedReimbursements(chainHeight, chainHeightOfPastCycle); // Param changes are taken into account at first block at next cycle after voting int firstBlockOfPastCycle = cyclesInDaoStateService.getHeightOfFirstBlockOfPastCycle(chainHeight, NUM_CYCLES_AVERAGE_DISTRIBUTION - 1); @@ -162,11 +165,31 @@ class BurnTargetService { .map(proposal -> (ReimbursementProposal) proposal); } - private long getAccumulatedReimbursements(int chainHeight, int fromBlock) { + private long getAdjustedAccumulatedReimbursements(int chainHeight, int fromBlock) { return getReimbursements(chainHeight).stream() .filter(reimbursementModel -> reimbursementModel.getHeight() > fromBlock) .filter(reimbursementModel -> reimbursementModel.getHeight() <= chainHeight) - .mapToLong(ReimbursementModel::getAmount) + .mapToLong(reimbursementModel -> { + long amount = reimbursementModel.getAmount(); + if (reimbursementModel.getHeight() > ACTIVATION_BLOCK) { + // As we do not pay out the losing party's security deposit we adjust this here. + // We use 15% as the min. security deposit as we do not have the detail data. + // A trade with 1 BTC has 1.3 BTC in the DPT which goes to BM. The reimbursement is + // only BSQ equivalent to 1.15 BTC. So we map back the 1.15 BTC to 1.3 BTC to account for + // that what the BM received. + // There are multiple unknowns included: + // - Real security deposit can be higher + // - Refund agent can make a custom payout, paying out more or less than expected + // - BSQ/BTC volatility + // - Delay between DPT and reimbursement + long adjusted = Math.round(amount * 1.3 / 1.15); + return adjusted; + } else { + // For old reimbursements we do not apply the adjustment as we had a different policy for + // reimbursing out 100% of the DPT. + return amount; + } + }) .sum(); } From 3ac6921d7e35206215a1c661913bdb185a9a6b97 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 15 Dec 2022 12:23:03 -0500 Subject: [PATCH 12/89] Remove numIssuance and numBurnOutputs columns to save space Signed-off-by: HenrikJannsen --- .../main/dao/burnbsq/burningman/BurningManView.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManView.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManView.java index 252fd97a8b..5b21909474 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManView.java @@ -878,7 +878,7 @@ public class BurningManView extends ActivatableView implements column.setComparator(Comparator.comparing(BurningManListItem::getAccumulatedBurnAmount)); column.setSortType(TableColumn.SortType.DESCENDING); - column = new AutoTooltipTableColumn<>(Res.get("dao.burningman.table.numBurnOutputs")); + /* column = new AutoTooltipTableColumn<>(Res.get("dao.burningman.table.numBurnOutputs")); column.setMinWidth(90); column.setMaxWidth(column.getMinWidth()); column.getStyleClass().add("last-column"); @@ -901,7 +901,7 @@ public class BurningManView extends ActivatableView implements }); burningManTableView.getColumns().add(column); column.setComparator(Comparator.comparing(BurningManListItem::getNumBurnOutputs)); - column.setSortType(TableColumn.SortType.DESCENDING); + column.setSortType(TableColumn.SortType.DESCENDING);*/ column = new AutoTooltipTableColumn<>(Res.get("dao.burningman.table.issuanceShare")); column.setMinWidth(110); @@ -975,7 +975,7 @@ public class BurningManView extends ActivatableView implements column.setComparator(Comparator.comparing(BurningManListItem::getAccumulatedCompensationAmount)); column.setSortType(TableColumn.SortType.DESCENDING); - column = new AutoTooltipTableColumn<>(Res.get("dao.burningman.table.numIssuances")); + /* column = new AutoTooltipTableColumn<>(Res.get("dao.burningman.table.numIssuances")); column.setMinWidth(110); column.setMaxWidth(column.getMinWidth()); column.getStyleClass().add("last-column"); @@ -998,7 +998,7 @@ public class BurningManView extends ActivatableView implements }); burningManTableView.getColumns().add(column); column.setComparator(Comparator.comparing(BurningManListItem::getNumIssuances)); - column.setSortType(TableColumn.SortType.DESCENDING); + column.setSortType(TableColumn.SortType.DESCENDING);*/ } private void createBurnOutputsColumns() { From 57147a672a63d9319e941c9230c0ea8122e2ce0c Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 15 Dec 2022 13:05:56 -0500 Subject: [PATCH 13/89] Use static fields for opReturnData instead of hardcoded mainnet hashes Signed-off-by: HenrikJannsen --- .../java/bisq/core/dao/burningman/BurnTargetService.java | 8 +++++--- .../dao/burningman/BurningManPresentationService.java | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/BurnTargetService.java b/core/src/main/java/bisq/core/dao/burningman/BurnTargetService.java index 6acc958759..45b2979dd2 100644 --- a/core/src/main/java/bisq/core/dao/burningman/BurnTargetService.java +++ b/core/src/main/java/bisq/core/dao/burningman/BurnTargetService.java @@ -45,6 +45,9 @@ import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; +import static bisq.core.dao.burningman.BurningManPresentationService.OP_RETURN_DATA_LEGACY_BM_DPT; +import static bisq.core.dao.burningman.BurningManPresentationService.OP_RETURN_DATA_LEGACY_BM_FEES; + /** * Burn target related API. Not touching trade protocol aspects and parameters can be changed here without risking to * break trade protocol validations. @@ -225,8 +228,7 @@ class BurnTargetService { .filter(tx -> tx.getBlockHeight() <= chainHeight) .filter(tx -> { String hash = Hex.encode(tx.getLastTxOutput().getOpReturnData()); - return "1701e47e5d8030f444c182b5e243871ebbaeadb5e82f".equals(hash) || - "1701293c488822f98e70e047012f46f5f1647f37deb7".equals(hash); + return OP_RETURN_DATA_LEGACY_BM_DPT.contains(hash); }) .mapToLong(Tx::getBurntBsq) .sum(); @@ -237,7 +239,7 @@ class BurnTargetService { return proofOfBurnTxs.stream() .filter(tx -> tx.getBlockHeight() > fromBlock) .filter(tx -> tx.getBlockHeight() <= chainHeight) - .filter(tx -> "1701721206fe6b40777763de1c741f4fd2706d94775d".equals(Hex.encode(tx.getLastTxOutput().getOpReturnData()))) + .filter(tx -> OP_RETURN_DATA_LEGACY_BM_FEES.contains(Hex.encode(tx.getLastTxOutput().getOpReturnData()))) .mapToLong(Tx::getBurntBsq) .sum(); } diff --git a/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java b/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java index aa4398c60e..9306daa380 100644 --- a/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java +++ b/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java @@ -69,14 +69,14 @@ public class BurningManPresentationService implements DaoStateListener { // Those are the opReturn data used by legacy BM for burning BTC received from DPT. // For regtest testing burn bsq and use the pre-image `dpt` which has the hash 14af04ea7e34bd7378b034ddf90da53b7c27a277. // The opReturn data gets additionally prefixed with 1701 - private static final Set OP_RETURN_DATA_LEGACY_BM_DPT = Config.baseCurrencyNetwork().isRegtest() ? + static final Set OP_RETURN_DATA_LEGACY_BM_DPT = Config.baseCurrencyNetwork().isRegtest() ? Set.of("170114af04ea7e34bd7378b034ddf90da53b7c27a277") : Set.of("1701e47e5d8030f444c182b5e243871ebbaeadb5e82f", "1701293c488822f98e70e047012f46f5f1647f37deb7"); // The opReturn data used by legacy BM for burning BTC received from BTC trade fees. // For regtest testing burn bsq and use the pre-image `fee` which has the hash b3253b7b92bb7f0916b05f10d4fa92be8e48f5e6. // The opReturn data gets additionally prefixed with 1701 - private static final Set OP_RETURN_DATA_LEGACY_BM_FEES = Config.baseCurrencyNetwork().isRegtest() ? + static final Set OP_RETURN_DATA_LEGACY_BM_FEES = Config.baseCurrencyNetwork().isRegtest() ? Set.of("1701b3253b7b92bb7f0916b05f10d4fa92be8e48f5e6") : Set.of("1701721206fe6b40777763de1c741f4fd2706d94775d"); From decd301108da97565ea6c65129c61ca21f734110 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 15 Dec 2022 15:50:05 -0500 Subject: [PATCH 14/89] Update BurningManAccountingStore_BTC_MAINNET Signed-off-by: HenrikJannsen --- .../BurningManAccountingStore_BTC_MAINNET | Bin 4749552 -> 4900390 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/p2p/src/main/resources/BurningManAccountingStore_BTC_MAINNET b/p2p/src/main/resources/BurningManAccountingStore_BTC_MAINNET index ab31521895e7d8d871bde7cf46f4759b9c43bdc1..f34d35c6c4d0051bfed83b40474f3e527d9e4430 100644 GIT binary patch delta 152250 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 b=Lpb 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

O->M6r+<4V5rBntWaHhHbF7^6=atQ3DBE59ZV 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 delta 238 zcmWN`yE=ja0D$4AzoJh_M1(>~Id!0fL=M#@SqcS~9tbO&1Ap#^ From 2012c9d09337af202f53b90ac58e966d63d73fe9 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 15 Dec 2022 17:46:47 -0500 Subject: [PATCH 15/89] Add new average bsq price after historical data Signed-off-by: HenrikJannsen --- .../accounting/BurningManAccountingService.java | 15 +++++++++++---- .../dao/burnbsq/burningman/BurningManView.java | 3 --- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/accounting/BurningManAccountingService.java b/core/src/main/java/bisq/core/dao/burningman/accounting/BurningManAccountingService.java index 46b61a5092..67a2380e19 100644 --- a/core/src/main/java/bisq/core/dao/burningman/accounting/BurningManAccountingService.java +++ b/core/src/main/java/bisq/core/dao/burningman/accounting/BurningManAccountingService.java @@ -67,13 +67,14 @@ public class BurningManAccountingService implements DaoSetupService { public static final int EARLIEST_BLOCK_HEIGHT = Config.baseCurrencyNetwork().isRegtest() ? 111 : 656035; public static final int EARLIEST_DATE_YEAR = 2020; public static final int EARLIEST_DATE_MONTH = 10; + public static final int HIST_BSQ_PRICE_LAST_DATE_YEAR = 2022; + public static final int HIST_BSQ_PRICE_LAST_DATE_MONTH = 10; private final BurningManAccountingStoreService burningManAccountingStoreService; private final BurningManPresentationService burningManPresentationService; private final TradeStatisticsManager tradeStatisticsManager; private final Preferences preferences; - @Getter private final Map averageBsqPriceByMonth = new HashMap<>(getHistoricalAverageBsqPriceByMonth()); @Getter private final Map balanceModelByBurningManName = new HashMap<>(); @@ -161,6 +162,12 @@ public class BurningManAccountingService implements DaoSetupService { return getBlocks().stream().filter(block -> block.getHeight() == height).findAny(); } + public Map getAverageBsqPriceByMonth() { + getAverageBsqPriceByMonth(new Date(), HIST_BSQ_PRICE_LAST_DATE_YEAR, HIST_BSQ_PRICE_LAST_DATE_MONTH) + .forEach((key, value) -> averageBsqPriceByMonth.put(new Date(key.getTime()), Price.valueOf("BSQ", value.getValue()))); + return averageBsqPriceByMonth; + } + /////////////////////////////////////////////////////////////////////////////////////////// // Delegates @@ -210,7 +217,7 @@ public class BurningManAccountingService implements DaoSetupService { }); } - private Map getAverageBsqPriceByMonth(Date from, int toYear, int toMonth) { + private Map getAverageBsqPriceByMonth(Date from, int backToYear, int backToMonth) { Map averageBsqPriceByMonth = new HashMap<>(); Calendar calendar = new GregorianCalendar(); calendar.setTime(from); @@ -218,7 +225,7 @@ public class BurningManAccountingService implements DaoSetupService { int month = calendar.get(Calendar.MONTH); do { for (; month >= 0; month--) { - if (year == toYear && month == toMonth) { + if (year == backToYear && month == backToMonth) { break; } Date date = DateUtil.getStartOfMonth(year, month); @@ -227,7 +234,7 @@ public class BurningManAccountingService implements DaoSetupService { } year--; month = 11; - } while (year >= toYear); + } while (year >= backToYear); return averageBsqPriceByMonth; } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManView.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManView.java index 5b21909474..16594e8fe6 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManView.java @@ -635,12 +635,9 @@ public class BurningManView extends ActivatableView implements } Map averageBsqPriceByMonth = burningManAccountingService.getAverageBsqPriceByMonth(); - long ts = System.currentTimeMillis(); balanceEntryObservableList.setAll(balanceEntries.stream() .map(balanceEntry -> new BalanceEntryItem(balanceEntry, averageBsqPriceByMonth, bsqFormatter, btcFormatter)) .collect(Collectors.toList())); - // 108869: 617 - 1878, 640-1531 - log.error("balanceEntryObservableList setAll took {} ms size={}", System.currentTimeMillis() - ts, balanceEntryObservableList.size()); } else { balanceEntryObservableList.clear(); } From 5dd82d7cb98cd9cf9bb946ca92b8dd5fc54abeda Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 15 Dec 2022 18:25:18 -0500 Subject: [PATCH 16/89] Increase GENESIS_OUTPUT_AMOUNT_FACTOR and ISSUANCE_BOOST_FACTOR Signed-off-by: HenrikJannsen --- .../java/bisq/core/dao/burningman/BurningManService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/BurningManService.java b/core/src/main/java/bisq/core/dao/burningman/BurningManService.java index 8d21627e27..8fc2a9bc58 100644 --- a/core/src/main/java/bisq/core/dao/burningman/BurningManService.java +++ b/core/src/main/java/bisq/core/dao/burningman/BurningManService.java @@ -80,7 +80,7 @@ public class BurningManService { static final String GENESIS_OUTPUT_PREFIX = "Bisq co-founder "; // Factor for weighting the genesis output amounts. - private static final double GENESIS_OUTPUT_AMOUNT_FACTOR = 0.05; + private static final double GENESIS_OUTPUT_AMOUNT_FACTOR = 0.1; // The number of cycles we go back for the decay function used for compensation request amounts. private static final int NUM_CYCLES_COMP_REQUEST_DECAY = 24; @@ -91,9 +91,9 @@ public class BurningManService { // Factor for boosting the issuance share (issuance is compensation requests + genesis output). // This will be used for increasing the allowed burn amount. The factor gives more flexibility // and compensates for those who do not burn. The burn share is capped by that factor as well. - // E.g. a contributor with 2% issuance share will be able to receive max 8% of the BTC fees or DPT output - // even if they had burned more and had a higher burn share than 8%. - public static final double ISSUANCE_BOOST_FACTOR = 4; + // E.g. a contributor with 1% issuance share will be able to receive max 10% of the BTC fees or DPT output + // even if they had burned more and had a higher burn share than 10%. + public static final double ISSUANCE_BOOST_FACTOR = 10; // The max amount the burn share can reach. This value is derived from the min. security deposit in a trade and // ensures that an attack where a BM would take all sell offers cannot be economically profitable as they would From b5dbce41fad77a5457d3a5cab035dc6e811e0299 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 15 Dec 2022 20:41:50 -0500 Subject: [PATCH 17/89] Add balance fields for DAO revenue with total burned BSQ and total distributed BTC/BSQ Signed-off-by: HenrikJannsen --- .../BurningManPresentationService.java | 6 ++++ .../BurningManAccountingService.java | 33 +++++++++++++++++++ .../resources/i18n/displayStrings.properties | 3 ++ .../burnbsq/burningman/BurningManView.java | 20 ++++++++++- 4 files changed, 61 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java b/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java index 9306daa380..7bfbe8fdb5 100644 --- a/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java +++ b/core/src/main/java/bisq/core/dao/burningman/BurningManPresentationService.java @@ -353,6 +353,12 @@ public class BurningManPresentationService implements DaoStateListener { return burningManNameByAddress; } + public long getTotalAmountOfBurnedBsq() { + return getBurningManCandidatesByName().values().stream() + .mapToLong(BurningManCandidate::getAccumulatedBurnAmount) + .sum(); + } + public String getGenesisTxId() { return daoStateService.getGenesisTxId(); } diff --git a/core/src/main/java/bisq/core/dao/burningman/accounting/BurningManAccountingService.java b/core/src/main/java/bisq/core/dao/burningman/accounting/BurningManAccountingService.java index 67a2380e19..35cf40d3c5 100644 --- a/core/src/main/java/bisq/core/dao/burningman/accounting/BurningManAccountingService.java +++ b/core/src/main/java/bisq/core/dao/burningman/accounting/BurningManAccountingService.java @@ -21,6 +21,7 @@ import bisq.core.dao.DaoSetupService; import bisq.core.dao.burningman.BurningManPresentationService; import bisq.core.dao.burningman.accounting.balance.BalanceEntry; import bisq.core.dao.burningman.accounting.balance.BalanceModel; +import bisq.core.dao.burningman.accounting.balance.BaseBalanceEntry; import bisq.core.dao.burningman.accounting.balance.ReceivedBtcBalanceEntry; import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock; import bisq.core.dao.burningman.accounting.blockchain.AccountingTx; @@ -35,6 +36,9 @@ import bisq.core.util.AveragePriceUtil; import bisq.common.UserThread; import bisq.common.config.Config; import bisq.common.util.DateUtil; +import bisq.common.util.MathUtils; + +import org.bitcoinj.core.Coin; import javax.inject.Inject; import javax.inject.Singleton; @@ -168,6 +172,35 @@ public class BurningManAccountingService implements DaoSetupService { return averageBsqPriceByMonth; } + public long getTotalAmountOfDistributedBtc() { + return balanceModelByBurningManName.values().stream() + .flatMap(balanceModel -> balanceModel.getReceivedBtcBalanceEntries().stream()) + .mapToLong(BaseBalanceEntry::getAmount) + .sum(); + } + + public long getTotalAmountOfDistributedBsq() { + Map averageBsqPriceByMonth = getAverageBsqPriceByMonth(); + return balanceModelByBurningManName.values().stream() + .flatMap(balanceModel -> balanceModel.getReceivedBtcBalanceEntries().stream()) + .map(balanceEntry -> { + Date month = balanceEntry.getMonth(); + Optional price = Optional.ofNullable(averageBsqPriceByMonth.get(month)); + long receivedBtc = balanceEntry.getAmount(); + Optional receivedBtcAsBsq; + if (price.isEmpty() || price.get().getValue() == 0) { + receivedBtcAsBsq = Optional.empty(); + } else { + long volume = price.get().getVolumeByAmount(Coin.valueOf(receivedBtc)).getValue(); + receivedBtcAsBsq = Optional.of(MathUtils.roundDoubleToLong(MathUtils.scaleDownByPowerOf10(volume, 6))); + } + return receivedBtcAsBsq; + }) + .filter(Optional::isPresent) + .mapToLong(Optional::get) + .sum(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Delegates diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 6c93723798..3896e386d6 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2299,6 +2299,9 @@ dao.burningman.burnTarget.fromTo={0} - {1} BSQ dao.burningman.filter=Filter burningman candidates dao.burningman.toggle=Show only active burningmen dao.burningman.contributorsComboBox.prompt=Select any of my contributors +dao.burningman.daoBalance=Balance for DAO +dao.burningman.daoBalanceTotalBurned=Total amount of burned BSQ +dao.burningman.daoBalanceTotalDistributed=Total amount of distributed BTC / BSQ dao.burningman.selectedContributor=Selected contributor dao.burningman.selectedContributorName=Contributor name dao.burningman.selectedContributorAddress=Receiver address diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManView.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManView.java index 16594e8fe6..d1b1e70830 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/burningman/BurningManView.java @@ -130,7 +130,7 @@ public class BurningManView extends ActivatableView implements private Button burnButton, exportBalanceEntriesButton; private TitledGroupBg burnOutputsTitledGroupBg, compensationsTitledGroupBg, selectedContributorTitledGroupBg; private AutoTooltipSlideToggleButton showOnlyActiveBurningmenToggle, showMonthlyBalanceEntryToggle; - private TextField expectedRevenueField, selectedContributorNameField, selectedContributorAddressField, burnTargetField; + private TextField expectedRevenueField, daoBalanceTotalBurnedField, daoBalanceTotalDistributedField, selectedContributorNameField, selectedContributorAddressField, burnTargetField; private ToggleGroup balanceEntryToggleGroup; private HBox balanceEntryHBox; private VBox selectedContributorNameBox, selectedContributorAddressBox; @@ -312,6 +312,20 @@ public class BurningManView extends ActivatableView implements HBox.setMargin(showOnlyActiveBurningmenToggle, new Insets(-21, 0, 0, 0)); hBox.getChildren().add(2, showOnlyActiveBurningmenToggle); + // DAO balance + addTitledGroupBg(gridPane, ++gridRow, 4, + Res.get("dao.burningman.daoBalance"), Layout.COMPACT_GROUP_DISTANCE); + daoBalanceTotalBurnedField = addCompactTopLabelTextField(gridPane, ++gridRow, + Res.get("dao.burningman.daoBalanceTotalBurned"), "", + Layout.COMPACT_GROUP_DISTANCE + Layout.FLOATING_LABEL_DISTANCE).second; + Tuple3 daoBalanceTotalDistributedTuple = addCompactTopLabelTextField(gridPane, gridRow, + Res.get("dao.burningman.daoBalanceTotalDistributed"), "", + Layout.COMPACT_GROUP_DISTANCE + Layout.FLOATING_LABEL_DISTANCE); + daoBalanceTotalDistributedField = daoBalanceTotalDistributedTuple.second; + VBox daoBalanceTotalDistributedBox = daoBalanceTotalDistributedTuple.third; + GridPane.setColumnSpan(daoBalanceTotalDistributedBox, 2); + GridPane.setColumnIndex(daoBalanceTotalDistributedBox, 1); + // Selected contributor selectedContributorTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 4, Res.get("dao.burningman.selectedContributor"), Layout.COMPACT_GROUP_DISTANCE); @@ -569,6 +583,10 @@ public class BurningManView extends ActivatableView implements .sorted(Comparator.comparing(BurningManListItem::getName)) .collect(Collectors.toList()); contributorComboBox.setItems(FXCollections.observableArrayList(myBurningManListItems)); + + daoBalanceTotalBurnedField.setText(bsqFormatter.formatCoinWithCode(burningManPresentationService.getTotalAmountOfBurnedBsq())); + daoBalanceTotalDistributedField.setText(btcFormatter.formatCoinWithCode(burningManAccountingService.getTotalAmountOfDistributedBtc()) + " / " + + bsqFormatter.formatCoinWithCode(burningManAccountingService.getTotalAmountOfDistributedBsq())); } } From 7754f798f7baad189c0a1715bec525aee19dce10 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Fri, 16 Dec 2022 17:25:23 +0100 Subject: [PATCH 18/89] Update sqrrm provided btc onion addresses --- core/src/main/java/bisq/core/btc/nodes/BtcNodes.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/btc/nodes/BtcNodes.java b/core/src/main/java/bisq/core/btc/nodes/BtcNodes.java index 8ca1c06429..5bfe593147 100644 --- a/core/src/main/java/bisq/core/btc/nodes/BtcNodes.java +++ b/core/src/main/java/bisq/core/btc/nodes/BtcNodes.java @@ -58,8 +58,8 @@ public class BtcNodes { new BtcNode("btc2.vante.me", "bsqbtcparrfihlwolt4xgjbf4cgqckvrvsfyvy6vhiqrnh4w6ghixoid.onion", "94.23.205.110", BtcNode.DEFAULT_PORT, "@miker"), // sqrrm - new BtcNode("btc1.sqrrm.net", "jygcc54etaubgdpcvzgbihjaqbc37cstpvum5sjzvka4bibkp4wrgnqd.onion", "185.25.48.184", BtcNode.DEFAULT_PORT, "@sqrrm"), - new BtcNode("btc2.sqrrm.net", "h32haomoe52ljz6qopedsocvotvoj5lm2zmecfhdhawb3flbsf64l2qd.onion", "81.171.22.143", BtcNode.DEFAULT_PORT, "@sqrrm"), + new BtcNode("btc1.sqrrm.net", "cwi3ekrwhig47dhhzfenr5hbvckj7fzaojygvazi2lucsenwbzwoyiqd.onion", "185.25.48.184", BtcNode.DEFAULT_PORT, "@sqrrm"), + new BtcNode("btc2.sqrrm.net", "upvthy74hgvgbqi6w3zd2mlchoi5tvvw7b5hpmmhcddd5fnnwrixneid.onion", "81.171.22.143", BtcNode.DEFAULT_PORT, "@sqrrm"), // Devin Bileck new BtcNode("btc1.bisq.services", "devinbtctu7uctl7hly2juu3thbgeivfnvw3ckj3phy6nyvpnx66yeyd.onion", "172.105.21.216", BtcNode.DEFAULT_PORT, "@devinbileck"), From 6fedc8177eca5c4e20f4403bfc939040b55369c4 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 16 Dec 2022 17:23:17 -0500 Subject: [PATCH 19/89] Increase timeouts from 3 to 4 min. Signed-off-by: HenrikJannsen --- .../core/dao/node/full/network/GetBlocksRequestHandler.java | 2 +- .../bisq/core/dao/node/lite/network/RequestBlocksHandler.java | 2 +- p2p/src/main/java/bisq/network/p2p/network/Connection.java | 2 +- .../bisq/network/p2p/peers/getdata/GetDataRequestHandler.java | 2 +- .../java/bisq/network/p2p/peers/getdata/RequestDataHandler.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/node/full/network/GetBlocksRequestHandler.java b/core/src/main/java/bisq/core/dao/node/full/network/GetBlocksRequestHandler.java index 98ce2db239..2013e5d5e7 100644 --- a/core/src/main/java/bisq/core/dao/node/full/network/GetBlocksRequestHandler.java +++ b/core/src/main/java/bisq/core/dao/node/full/network/GetBlocksRequestHandler.java @@ -49,7 +49,7 @@ import org.jetbrains.annotations.NotNull; */ @Slf4j class GetBlocksRequestHandler { - private static final long TIMEOUT_MIN = 3; + private static final long TIMEOUT_MIN = 4; /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/dao/node/lite/network/RequestBlocksHandler.java b/core/src/main/java/bisq/core/dao/node/lite/network/RequestBlocksHandler.java index 752e4829f8..8c72b8fb2c 100644 --- a/core/src/main/java/bisq/core/dao/node/lite/network/RequestBlocksHandler.java +++ b/core/src/main/java/bisq/core/dao/node/lite/network/RequestBlocksHandler.java @@ -51,7 +51,7 @@ import org.jetbrains.annotations.Nullable; */ @Slf4j public class RequestBlocksHandler implements MessageListener { - private static final long TIMEOUT_MIN = 3; + private static final long TIMEOUT_MIN = 4; /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/p2p/src/main/java/bisq/network/p2p/network/Connection.java b/p2p/src/main/java/bisq/network/p2p/network/Connection.java index 757879d23f..be0a3390d6 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/Connection.java +++ b/p2p/src/main/java/bisq/network/p2p/network/Connection.java @@ -108,7 +108,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { private static final int PERMITTED_MESSAGE_SIZE = 200 * 1024; // 200 kb private static final int MAX_PERMITTED_MESSAGE_SIZE = 10 * 1024 * 1024; // 10 MB (425 offers resulted in about 660 kb, mailbox msg will add more to it) offer has usually 2 kb, mailbox 3kb. //TODO decrease limits again after testing - private static final int SOCKET_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(180); + private static final int SOCKET_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(240); public static int getPermittedMessageSize() { return PERMITTED_MESSAGE_SIZE; diff --git a/p2p/src/main/java/bisq/network/p2p/peers/getdata/GetDataRequestHandler.java b/p2p/src/main/java/bisq/network/p2p/peers/getdata/GetDataRequestHandler.java index 9e453f985b..02aab8e164 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/getdata/GetDataRequestHandler.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/getdata/GetDataRequestHandler.java @@ -41,7 +41,7 @@ import org.jetbrains.annotations.NotNull; @Slf4j public class GetDataRequestHandler { - private static final long TIMEOUT = 180; + private static final long TIMEOUT = 240; private static final int MAX_ENTRIES = 5000; diff --git a/p2p/src/main/java/bisq/network/p2p/peers/getdata/RequestDataHandler.java b/p2p/src/main/java/bisq/network/p2p/peers/getdata/RequestDataHandler.java index d9140f6a9a..42b3ee67d8 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/getdata/RequestDataHandler.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/getdata/RequestDataHandler.java @@ -54,7 +54,7 @@ import org.jetbrains.annotations.Nullable; @Slf4j class RequestDataHandler implements MessageListener { - private static final long TIMEOUT = 180; + private static final long TIMEOUT = 240; private NodeAddress peersNodeAddress; private String getDataRequestType; From 14c188afbe0f099baf48c16075702ecbd1c91705 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 16 Dec 2022 17:38:23 -0500 Subject: [PATCH 20/89] Use ThreadPoolExecutor with custom set queueCapacity instead of CachedThreadPool The previously used newCachedThreadPool carries higher risk for execution exceptions if exceeded. Originally we had only one executor with a corePoolSize of 15 and a maximumPoolSize of 30 and queueCapacity was set to maximumPoolSize. This was risky when the 15 corePool threads have been busy and new messages or connection creation threads are queued up with potentially significant delay until getting served leading to timeouts. Now we use (if maxConnections is 12) corePoolSize of 24, maximumPoolSize 36 and queueCapacity 10. This gives considerable headroom. We also have split up the executors in 2 distinct ones. Signed-off-by: HenrikJannsen --- .../main/java/bisq/common/util/Utilities.java | 38 +++++++++---------- .../bisq/network/p2p/network/NetworkNode.java | 12 +++++- .../reporting/SeedNodeReportingService.java | 2 +- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/common/src/main/java/bisq/common/util/Utilities.java b/common/src/main/java/bisq/common/util/Utilities.java index 6eebc477ff..8343f0326c 100644 --- a/common/src/main/java/bisq/common/util/Utilities.java +++ b/common/src/main/java/bisq/common/util/Utilities.java @@ -64,7 +64,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -101,7 +100,15 @@ public class Utilities { int corePoolSize, int maximumPoolSize, long keepAliveTimeInSec) { - return MoreExecutors.listeningDecorator(getThreadPoolExecutor(name, corePoolSize, maximumPoolSize, keepAliveTimeInSec)); + return getListeningExecutorService(name, corePoolSize, maximumPoolSize, maximumPoolSize, keepAliveTimeInSec); + } + + public static ListeningExecutorService getListeningExecutorService(String name, + int corePoolSize, + int maximumPoolSize, + int queueCapacity, + long keepAliveTimeInSec) { + return MoreExecutors.listeningDecorator(getThreadPoolExecutor(name, corePoolSize, maximumPoolSize, queueCapacity, keepAliveTimeInSec)); } public static ListeningExecutorService getListeningExecutorService(String name, @@ -116,8 +123,17 @@ public class Utilities { int corePoolSize, int maximumPoolSize, long keepAliveTimeInSec) { + return getThreadPoolExecutor(name, corePoolSize, maximumPoolSize, maximumPoolSize, keepAliveTimeInSec); + } + + + public static ThreadPoolExecutor getThreadPoolExecutor(String name, + int corePoolSize, + int maximumPoolSize, + int queueCapacity, + long keepAliveTimeInSec) { return getThreadPoolExecutor(name, corePoolSize, maximumPoolSize, keepAliveTimeInSec, - new ArrayBlockingQueue<>(maximumPoolSize)); + new ArrayBlockingQueue<>(queueCapacity)); } private static ThreadPoolExecutor getThreadPoolExecutor(String name, @@ -135,22 +151,6 @@ public class Utilities { return executor; } - public static ExecutorService newCachedThreadPool(String name, - int maximumPoolSize, - long keepAliveTime, - TimeUnit timeUnit) { - ThreadFactory threadFactory = new ThreadFactoryBuilder() - .setNameFormat(name + "-%d") - .setDaemon(true) - .build(); - return new ThreadPoolExecutor(0, - maximumPoolSize, - keepAliveTime, - timeUnit, - new SynchronousQueue<>(), - threadFactory); - } - @SuppressWarnings("SameParameterValue") public static ScheduledThreadPoolExecutor getScheduledThreadPoolExecutor(String name, int corePoolSize, diff --git a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java index 21a199ffa2..5b60792a2e 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java +++ b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java @@ -100,8 +100,16 @@ public abstract class NetworkNode implements MessageListener { this.networkProtoResolver = networkProtoResolver; this.networkFilter = networkFilter; - connectionExecutor = MoreExecutors.listeningDecorator(Utilities.newCachedThreadPool("NetworkNode.connection", maxConnections * 2, 1, TimeUnit.MINUTES)); - sendMessageExecutor = MoreExecutors.listeningDecorator(Utilities.newCachedThreadPool("NetworkNode.sendMessage", maxConnections * 2, 3, TimeUnit.MINUTES)); + connectionExecutor = Utilities.getListeningExecutorService("NetworkNode.connection", + maxConnections * 2, + maxConnections * 3, + 10, + 60); + sendMessageExecutor = Utilities.getListeningExecutorService("NetworkNode.sendMessage", + maxConnections * 2, + maxConnections * 3, + 10, + 60); serverExecutor = Utilities.getSingleThreadExecutor("NetworkNode.server-" + servicePort); } diff --git a/seednode/src/main/java/bisq/seednode/reporting/SeedNodeReportingService.java b/seednode/src/main/java/bisq/seednode/reporting/SeedNodeReportingService.java index bb77794fb3..de03faecb3 100644 --- a/seednode/src/main/java/bisq/seednode/reporting/SeedNodeReportingService.java +++ b/seednode/src/main/java/bisq/seednode/reporting/SeedNodeReportingService.java @@ -118,7 +118,7 @@ public class SeedNodeReportingService { // The pool size must be larger as the expected parallel sends because HttpClient use it // internally for asynchronous and dependent tasks. - executor = Utilities.newCachedThreadPool("SeedNodeReportingService", 20, 8, TimeUnit.MINUTES); + executor = Utilities.getThreadPoolExecutor("SeedNodeReportingService", 20, 40, 100, 8 * 60); httpClient = HttpClient.newBuilder().executor(executor).build(); heartBeatTimer = UserThread.runPeriodically(this::sendHeartBeat, HEART_BEAT_DELAY_SEC); From 049caab89cf21d8a9a0777c146b3f36eb9a47d1e Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 16 Dec 2022 17:45:10 -0500 Subject: [PATCH 21/89] Reduce getDataResponse size from 9 MB to 6 MB Reduce number of blocks at GetBlocksResponse from 4000 to 3000 Signed-off-by: HenrikJannsen --- .../core/dao/node/full/network/GetBlocksRequestHandler.java | 4 ++-- .../main/java/bisq/network/p2p/storage/P2PDataStorage.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/node/full/network/GetBlocksRequestHandler.java b/core/src/main/java/bisq/core/dao/node/full/network/GetBlocksRequestHandler.java index 98ce2db239..a4b25b9bfa 100644 --- a/core/src/main/java/bisq/core/dao/node/full/network/GetBlocksRequestHandler.java +++ b/core/src/main/java/bisq/core/dao/node/full/network/GetBlocksRequestHandler.java @@ -91,8 +91,8 @@ class GetBlocksRequestHandler { public void onGetBlocksRequest(GetBlocksRequest getBlocksRequest, Connection connection) { long ts = System.currentTimeMillis(); - // We limit number of blocks to 4000 which is about 1 month. - List blocks = new LinkedList<>(daoStateService.getBlocksFromBlockHeight(getBlocksRequest.getFromBlockHeight(), 4000)); + // We limit number of blocks to 3000 which is about 3 weeks. + List blocks = new LinkedList<>(daoStateService.getBlocksFromBlockHeight(getBlocksRequest.getFromBlockHeight(), 3000)); List rawBlocks = blocks.stream().map(RawBlock::fromBlock).collect(Collectors.toList()); GetBlocksResponse getBlocksResponse = new GetBlocksResponse(rawBlocks, getBlocksRequest.getNonce()); log.info("Received GetBlocksRequest from {} for blocks from height {}. " + diff --git a/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java b/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java index 5b9e46a41d..023a231061 100644 --- a/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java +++ b/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java @@ -312,7 +312,7 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers Map mapForDataResponse = getMapForDataResponse(getDataRequest.getVersion()); // Give a bit of tolerance for message overhead - double maxSize = Connection.getMaxPermittedMessageSize() * 0.9; + double maxSize = Connection.getMaxPermittedMessageSize() * 0.6; // 25% of space is allocated for PersistableNetworkPayloads long limit = Math.round(maxSize * 0.25); From a09dae16bec982e240cf36ac2acf29da06020fc7 Mon Sep 17 00:00:00 2001 From: jmacxx <47253594+jmacxx@users.noreply.github.com> Date: Fri, 16 Dec 2022 20:36:03 -0600 Subject: [PATCH 22/89] Fix divide by zero errors in Trade history summary. --- .../java/bisq/core/trade/ClosedTradableFormatter.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/bisq/core/trade/ClosedTradableFormatter.java b/core/src/main/java/bisq/core/trade/ClosedTradableFormatter.java index 23a6059a06..b64b2880f3 100644 --- a/core/src/main/java/bisq/core/trade/ClosedTradableFormatter.java +++ b/core/src/main/java/bisq/core/trade/ClosedTradableFormatter.java @@ -97,7 +97,8 @@ public class ClosedTradableFormatter { } public String getTotalTxFeeAsString(Coin totalTradeAmount, Coin totalTxFee) { - double percentage = ((double) totalTxFee.value) / totalTradeAmount.value; + double percentage = Math.abs(totalTradeAmount.value) > 0 ? // protect from divide-by-zero + ((double) totalTxFee.value) / totalTradeAmount.value : 0; return Res.get(I18N_KEY_TOTAL_TX_FEE, btcFormatter.formatCoin(totalTxFee, true), formatToPercentWithSymbol(percentage)); @@ -116,7 +117,8 @@ public class ClosedTradableFormatter { public String getTotalTradeFeeInBsqAsString(Coin totalTradeFee, Volume tradeAmountVolume, Volume bsqVolumeInUsd) { - double percentage = ((double) bsqVolumeInUsd.getValue()) / tradeAmountVolume.getValue(); + double percentage = Math.abs(tradeAmountVolume.getValue()) > 0 ? // protect from divide-by-zero + ((double) bsqVolumeInUsd.getValue()) / tradeAmountVolume.getValue() : 0; return Res.get(I18N_KEY_TOTAL_TRADE_FEE_BSQ, bsqFormatter.formatCoin(totalTradeFee, true), formatToPercentWithSymbol(percentage)); @@ -132,7 +134,8 @@ public class ClosedTradableFormatter { } public String getTotalTradeFeeInBtcAsString(Coin totalTradeAmount, Coin totalTradeFee) { - double percentage = ((double) totalTradeFee.value) / totalTradeAmount.value; + double percentage = Math.abs(totalTradeAmount.value) > 0 ? // protect from divide-by-zero + ((double) totalTradeFee.value) / totalTradeAmount.value : 0; return Res.get(I18N_KEY_TOTAL_TRADE_FEE_BTC, btcFormatter.formatCoin(totalTradeFee, true), formatToPercentWithSymbol(percentage)); From 9efab7e7dea16bc3a941ba85322e0660c5fb2fb2 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Sat, 17 Dec 2022 20:00:43 -0500 Subject: [PATCH 23/89] Exclude legacy BM from DAO balance Signed-off-by: HenrikJannsen --- .../accounting/BurningManAccountingService.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/accounting/BurningManAccountingService.java b/core/src/main/java/bisq/core/dao/burningman/accounting/BurningManAccountingService.java index 35cf40d3c5..64331746bf 100644 --- a/core/src/main/java/bisq/core/dao/burningman/accounting/BurningManAccountingService.java +++ b/core/src/main/java/bisq/core/dao/burningman/accounting/BurningManAccountingService.java @@ -54,6 +54,7 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; +import java.util.stream.Stream; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -173,16 +174,14 @@ public class BurningManAccountingService implements DaoSetupService { } public long getTotalAmountOfDistributedBtc() { - return balanceModelByBurningManName.values().stream() - .flatMap(balanceModel -> balanceModel.getReceivedBtcBalanceEntries().stream()) + return getReceivedBtcBalanceEntryStreamExcludingLegacyBurningmen() .mapToLong(BaseBalanceEntry::getAmount) .sum(); } public long getTotalAmountOfDistributedBsq() { Map averageBsqPriceByMonth = getAverageBsqPriceByMonth(); - return balanceModelByBurningManName.values().stream() - .flatMap(balanceModel -> balanceModel.getReceivedBtcBalanceEntries().stream()) + return getReceivedBtcBalanceEntryStreamExcludingLegacyBurningmen() .map(balanceEntry -> { Date month = balanceEntry.getMonth(); Optional price = Optional.ofNullable(averageBsqPriceByMonth.get(month)); @@ -201,6 +200,14 @@ public class BurningManAccountingService implements DaoSetupService { .sum(); } + private Stream getReceivedBtcBalanceEntryStreamExcludingLegacyBurningmen() { + return balanceModelByBurningManName.entrySet().stream() + .filter(e -> !e.getKey().equals(BurningManPresentationService.LEGACY_BURNING_MAN_DPT_NAME) && + !e.getKey().equals(BurningManPresentationService.LEGACY_BURNING_MAN_BTC_FEES_NAME)) + .map(Map.Entry::getValue) + .flatMap(balanceModel -> balanceModel.getReceivedBtcBalanceEntries().stream()); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Delegates From 72372970f83da0465515224e4eceb4189bbf1d9a Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Sat, 17 Dec 2022 20:15:28 -0500 Subject: [PATCH 24/89] Add sanity check that max share of a non-legacy BM is 20% over MAX_BURN_SHARE (taking into account potential increase due adjustment) Signed-off-by: HenrikJannsen --- .../core/dao/burningman/DelayedPayoutTxReceiverService.java | 3 +++ .../bisq_v1/tasks/seller/SellerCreatesDelayedPayoutTx.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java index 288b8f5bb6..50cce60f19 100644 --- a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java +++ b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java @@ -130,6 +130,8 @@ public class DelayedPayoutTxReceiverService implements DaoStateListener { // We only use outputs > 1000 sat or at least 2 times the cost for the output (32 bytes). // If we remove outputs it will be spent as miner fee. long minOutputAmount = Math.max(DPT_MIN_OUTPUT_AMOUNT, txFeePerVbyte * 32 * 2); + // Sanity check that max share of a non-legacy BM is 20% over MAX_BURN_SHARE (taking into account potential increase due adjustment) + long maxOutputAmount = Math.round(inputAmount * (BurningManService.MAX_BURN_SHARE * 1.2)); // We accumulate small amounts which gets filtered out and subtract it from 1 to get an adjustment factor // used later to be applied to the remaining burningmen share. double adjustment = 1 - burningManCandidates.stream() @@ -149,6 +151,7 @@ public class DelayedPayoutTxReceiverService implements DaoStateListener { candidate.getMostRecentAddress().get()); }) .filter(tuple -> tuple.first >= minOutputAmount) + .filter(tuple -> tuple.first <= maxOutputAmount) .sorted(Comparator., Long>comparing(tuple -> tuple.first) .thenComparing(tuple -> tuple.second)) .collect(Collectors.toList()); diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/seller/SellerCreatesDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/seller/SellerCreatesDelayedPayoutTx.java index 9a0346fab1..e8d73c365d 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/seller/SellerCreatesDelayedPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/seller/SellerCreatesDelayedPayoutTx.java @@ -59,7 +59,7 @@ public class SellerCreatesDelayedPayoutTx extends TradeTask { selectionHeight, inputAmount, tradeTxFeeAsLong); - log.info("Verify delayedPayoutTx using selectionHeight {} and receivers {}", selectionHeight, delayedPayoutTxReceivers); + log.info("Create delayedPayoutTx using selectionHeight {} and receivers {}", selectionHeight, delayedPayoutTxReceivers); long lockTime = trade.getLockTime(); preparedDelayedPayoutTx = tradeWalletService.createDelayedUnsignedPayoutTx( depositTx, From c36c9b214ca2b3ed19928471000521f30e2ce1bf Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Sun, 18 Dec 2022 12:27:29 -0500 Subject: [PATCH 25/89] Add sanity check for a min. block height for the snapshot height We don't allow to get further back than 767950 (the block height from Dec. 18th 2022) Signed-off-by: HenrikJannsen --- .../burningman/DelayedPayoutTxReceiverService.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java index 50cce60f19..83c02ed1e5 100644 --- a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java +++ b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java @@ -23,6 +23,7 @@ import bisq.core.dao.state.DaoStateListener; import bisq.core.dao.state.DaoStateService; import bisq.core.dao.state.model.blockchain.Block; +import bisq.common.config.Config; import bisq.common.util.Tuple2; import javax.inject.Inject; @@ -37,6 +38,8 @@ import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; +import static com.google.common.base.Preconditions.checkArgument; + /** * Used in the trade protocol for creating and verifying the delayed payout transaction. * Requires to be deterministic. @@ -46,6 +49,9 @@ import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class DelayedPayoutTxReceiverService implements DaoStateListener { + // We don't allow to get further back than 767950 (the block height from Dec. 18th 2022). + static final int MIN_SNAPSHOT_HEIGHT = Config.baseCurrencyNetwork().isRegtest() ? 111 : 767950; + // One part of the limit for the min. amount to be included in the DPT outputs. // The miner fee rate multiplied by 2 times the output size is the other factor. // The higher one of both is used. 1000 sat is about 2 USD @ 20k price. @@ -107,6 +113,8 @@ public class DelayedPayoutTxReceiverService implements DaoStateListener { public List> getReceivers(int burningManSelectionHeight, long inputAmount, long tradeTxFee) { + + checkArgument(burningManSelectionHeight >= MIN_SNAPSHOT_HEIGHT, "Selection height must be >= " + MIN_SNAPSHOT_HEIGHT); Collection burningManCandidates = burningManService.getBurningManCandidatesByName(burningManSelectionHeight).values(); if (burningManCandidates.isEmpty()) { // If there are no compensation requests (e.g. at dev testing) we fall back to the legacy BM @@ -183,9 +191,9 @@ public class DelayedPayoutTxReceiverService implements DaoStateListener { int minSnapshotHeight = genesisHeight + 3 * grid; if (height > minSnapshotHeight) { int ratio = (int) Math.round(height / (double) grid); - return ratio * grid - grid; + return Math.max(MIN_SNAPSHOT_HEIGHT, ratio * grid - grid); } else { - return genesisHeight; + return Math.max(MIN_SNAPSHOT_HEIGHT, genesisHeight); } } } From 2a8d1b34267fb4744cfdcf0dde9d5e4ad15cfbc2 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Sun, 18 Dec 2022 12:57:15 -0500 Subject: [PATCH 26/89] Fix test Signed-off-by: HenrikJannsen --- .../DelayedPayoutTxReceiverService.java | 15 ++++--- .../DelayedPayoutTxReceiverServiceTest.java | 42 +++++++++---------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java index 83c02ed1e5..1f3056fec2 100644 --- a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java +++ b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java @@ -50,7 +50,7 @@ import static com.google.common.base.Preconditions.checkArgument; @Singleton public class DelayedPayoutTxReceiverService implements DaoStateListener { // We don't allow to get further back than 767950 (the block height from Dec. 18th 2022). - static final int MIN_SNAPSHOT_HEIGHT = Config.baseCurrencyNetwork().isRegtest() ? 111 : 767950; + static final int MIN_SNAPSHOT_HEIGHT = Config.baseCurrencyNetwork().isRegtest() ? 0 : 767950; // One part of the limit for the min. amount to be included in the DPT outputs. // The miner fee rate multiplied by 2 times the output size is the other factor. @@ -185,15 +185,18 @@ public class DelayedPayoutTxReceiverService implements DaoStateListener { return inputAmount - minerFee; } + private static int getSnapshotHeight(int genesisHeight, int height, int grid) { + return getSnapshotHeight(genesisHeight, height, grid, MIN_SNAPSHOT_HEIGHT); + } + // Borrowed from DaoStateSnapshotService. We prefer to not reuse to avoid dependency to an unrelated domain. @VisibleForTesting - static int getSnapshotHeight(int genesisHeight, int height, int grid) { - int minSnapshotHeight = genesisHeight + 3 * grid; - if (height > minSnapshotHeight) { + static int getSnapshotHeight(int genesisHeight, int height, int grid, int minSnapshotHeight) { + if (height > (genesisHeight + 3 * grid)) { int ratio = (int) Math.round(height / (double) grid); - return Math.max(MIN_SNAPSHOT_HEIGHT, ratio * grid - grid); + return Math.max(minSnapshotHeight, ratio * grid - grid); } else { - return Math.max(MIN_SNAPSHOT_HEIGHT, genesisHeight); + return Math.max(minSnapshotHeight, genesisHeight); } } } diff --git a/core/src/test/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverServiceTest.java b/core/src/test/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverServiceTest.java index 669d9c92b8..11d95bb746 100644 --- a/core/src/test/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverServiceTest.java +++ b/core/src/test/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverServiceTest.java @@ -30,31 +30,31 @@ public class DelayedPayoutTxReceiverServiceTest { @Test public void testGetSnapshotHeight() { // up to genesis + 3* grid we use genesis - assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 0, 10)); - assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 100, 10)); - assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 102, 10)); - assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 119, 10)); - assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 120, 10)); - assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 121, 10)); - assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 130, 10)); - assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 131, 10)); - assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 132, 10)); + assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 0, 10, 0)); + assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 100, 10, 0)); + assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 102, 10, 0)); + assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 119, 10, 0)); + assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 120, 10, 0)); + assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 121, 10, 0)); + assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 130, 10, 0)); + assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 131, 10, 0)); + assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 132, 10, 0)); - assertEquals(120, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 133, 10)); - assertEquals(120, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 134, 10)); + assertEquals(120, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 133, 10, 0)); + assertEquals(120, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 134, 10, 0)); - assertEquals(130, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 135, 10)); - assertEquals(130, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 136, 10)); - assertEquals(130, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 139, 10)); - assertEquals(130, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 140, 10)); - assertEquals(130, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 141, 10)); + assertEquals(130, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 135, 10, 0)); + assertEquals(130, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 136, 10, 0)); + assertEquals(130, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 139, 10, 0)); + assertEquals(130, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 140, 10, 0)); + assertEquals(130, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 141, 10, 0)); - assertEquals(140, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 149, 10)); - assertEquals(140, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 150, 10)); - assertEquals(140, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 151, 10)); + assertEquals(140, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 149, 10, 0)); + assertEquals(140, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 150, 10, 0)); + assertEquals(140, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 151, 10, 0)); - assertEquals(150, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 159, 10)); + assertEquals(150, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 159, 10, 0)); - assertEquals(990, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 1000, 10)); + assertEquals(990, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 1000, 10, 0)); } } From 21541d68800cbd81a8d5ba8d5ebd183eda99d4ea Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Sun, 18 Dec 2022 13:13:36 -0500 Subject: [PATCH 27/89] Add INVALID_SNAPSHOT_HEIGHT to AvailabilityResult. Use AvailabilityResult.INVALID_SNAPSHOT_HEIGHT instead of AckMessage with error. Show description in error popup instead of enum name. Return PRICE_CHECK_FAILED instead of UNKNOWN_FAILURE at error at price check also for non api users. Signed-off-by: HenrikJannsen --- .../bisq/core/offer/OpenOfferManager.java | 38 +++++++++---------- .../availability/AvailabilityResult.java | 36 +++++++++--------- .../ProcessOfferAvailabilityResponse.java | 2 +- .../daemon/grpc/GrpcErrorMessageHandler.java | 2 +- proto/src/main/proto/pb.proto | 1 + 5 files changed, 38 insertions(+), 41 deletions(-) diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index c3ab839966..06a85ae6ab 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -673,22 +673,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return; } - if (BurningManService.isActivated()) { - try { - int takersBurningManSelectionHeight = request.getBurningManSelectionHeight(); - checkArgument(takersBurningManSelectionHeight > 0, "takersBurningManSelectionHeight must not be 0"); - - int makersBurningManSelectionHeight = delayedPayoutTxReceiverService.getBurningManSelectionHeight(); - checkArgument(takersBurningManSelectionHeight == makersBurningManSelectionHeight, - "takersBurningManSelectionHeight does no match makersBurningManSelectionHeight"); - } catch (Throwable t) { - errorMessage = "Message validation failed. Error=" + t + ", Message=" + request; - log.warn(errorMessage); - sendAckMessage(request, peer, false, errorMessage); - return; - } - } - try { Optional openOfferOptional = getOpenOfferById(request.offerId); AvailabilityResult availabilityResult; @@ -721,11 +705,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe availabilityResult = AvailabilityResult.MARKET_PRICE_NOT_AVAILABLE; } catch (Throwable e) { log.warn("Trade price check failed. " + e.getMessage()); - if (coreContext.isApiUser()) - // Give api user something more than 'unknown_failure'. - availabilityResult = AvailabilityResult.PRICE_CHECK_FAILED; - else - availabilityResult = AvailabilityResult.UNKNOWN_FAILURE; + availabilityResult = AvailabilityResult.PRICE_CHECK_FAILED; } } else { availabilityResult = AvailabilityResult.USER_IGNORED; @@ -747,6 +727,22 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe availabilityResult = AvailabilityResult.UNCONF_TX_LIMIT_HIT; } + if (BurningManService.isActivated()) { + try { + int takersBurningManSelectionHeight = request.getBurningManSelectionHeight(); + checkArgument(takersBurningManSelectionHeight > 0, "takersBurningManSelectionHeight must not be 0"); + + int makersBurningManSelectionHeight = delayedPayoutTxReceiverService.getBurningManSelectionHeight(); + checkArgument(takersBurningManSelectionHeight == makersBurningManSelectionHeight, + "takersBurningManSelectionHeight does no match makersBurningManSelectionHeight. " + + "takersBurningManSelectionHeight=" + takersBurningManSelectionHeight + "; makersBurningManSelectionHeight=" + makersBurningManSelectionHeight); + } catch (Throwable t) { + errorMessage = "Message validation failed. Error=" + t + ", Message=" + request; + log.warn(errorMessage); + availabilityResult = AvailabilityResult.INVALID_SNAPSHOT_HEIGHT; + } + } + OfferAvailabilityResponse offerAvailabilityResponse = new OfferAvailabilityResponse(request.offerId, availabilityResult, arbitratorNodeAddress, diff --git a/core/src/main/java/bisq/core/offer/availability/AvailabilityResult.java b/core/src/main/java/bisq/core/offer/availability/AvailabilityResult.java index abf3bd33cf..a2c76e0d0e 100644 --- a/core/src/main/java/bisq/core/offer/availability/AvailabilityResult.java +++ b/core/src/main/java/bisq/core/offer/availability/AvailabilityResult.java @@ -17,31 +17,31 @@ package bisq.core.offer.availability; -public enum AvailabilityResult { - UNKNOWN_FAILURE("cannot take offer for unknown reason"), - AVAILABLE("offer available"), - OFFER_TAKEN("offer taken"), - PRICE_OUT_OF_TOLERANCE("cannot take offer because taker's price is outside tolerance"), - MARKET_PRICE_NOT_AVAILABLE("cannot take offer because market price for calculating trade price is unavailable"), - @SuppressWarnings("unused") NO_ARBITRATORS("cannot take offer because no arbitrators are available"), - NO_MEDIATORS("cannot take offer because no mediators are available"), - USER_IGNORED("cannot take offer because user is ignored"), - @SuppressWarnings("unused") MISSING_MANDATORY_CAPABILITY("description not available"), - @SuppressWarnings("unused") NO_REFUND_AGENTS("cannot take offer because no refund agents are available"), - UNCONF_TX_LIMIT_HIT("cannot take offer because you have too many unconfirmed transactions at this moment"), - MAKER_DENIED_API_USER("cannot take offer because maker is api user"), - PRICE_CHECK_FAILED("cannot take offer because trade price check failed"); +import lombok.Getter; +public enum AvailabilityResult { + UNKNOWN_FAILURE("Cannot take offer for unknown reason"), + AVAILABLE("Offer is available"), + OFFER_TAKEN("Offer is taken"), + PRICE_OUT_OF_TOLERANCE("Cannot take offer because taker's price is outside tolerance"), + MARKET_PRICE_NOT_AVAILABLE("Cannot take offer because market price for calculating trade price is unavailable"), + @SuppressWarnings("unused") NO_ARBITRATORS("Cannot take offer because no arbitrators are available"), + NO_MEDIATORS("Cannot take offer because no mediators are available"), + USER_IGNORED("Cannot take offer because user is ignored"), + @SuppressWarnings("unused") MISSING_MANDATORY_CAPABILITY("Missing mandatory capability"), + @SuppressWarnings("unused") NO_REFUND_AGENTS("Cannot take offer because no refund agents are available"), + UNCONF_TX_LIMIT_HIT("Cannot take offer because you have too many unconfirmed transactions at this moment"), + MAKER_DENIED_API_USER("Cannot take offer because maker is api user"), + PRICE_CHECK_FAILED("Cannot take offer because trade price check failed"), + INVALID_SNAPSHOT_HEIGHT("Cannot take offer because snapshot height does not match. Probably your DAO data are not synced."); + + @Getter private final String description; AvailabilityResult(String description) { this.description = description; } - public String description() { - return description; - } - public static AvailabilityResult fromProto(protobuf.AvailabilityResult proto) { return AvailabilityResult.valueOf(proto.name()); } diff --git a/core/src/main/java/bisq/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java b/core/src/main/java/bisq/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java index 3afb6fb2a3..403230806a 100644 --- a/core/src/main/java/bisq/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java +++ b/core/src/main/java/bisq/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java @@ -51,7 +51,7 @@ public class ProcessOfferAvailabilityResponse extends Task Date: Mon, 19 Dec 2022 14:03:50 +0000 Subject: [PATCH 28/89] Bump actions/setup-java from 3.8.0 to 3.9.0 Bumps [actions/setup-java](https://github.com/actions/setup-java) from 3.8.0 to 3.9.0. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/c3ac5dd0ed8db40fedb61c32fbe677e6b355e94c...1df8dbefe2a8cbc99770194893dd902763bee34b) --- updated-dependencies: - dependency-name: actions/setup-java dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4f129093d5..684c26742a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Set up JDK - uses: actions/setup-java@c3ac5dd0ed8db40fedb61c32fbe677e6b355e94c + uses: actions/setup-java@1df8dbefe2a8cbc99770194893dd902763bee34b with: java-version: ${{ matrix.java }} distribution: 'zulu' From c9b6a423dcb8133e8da86ab66eb33972205a5832 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Dec 2022 14:03:53 +0000 Subject: [PATCH 29/89] Bump actions/checkout from 3.1.0 to 3.2.0 Bumps [actions/checkout](https://github.com/actions/checkout) from 3.1.0 to 3.2.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8...755da8c3cf115ac066823e79a1e1788f8940201b) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4f129093d5..0e12d3683f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: java: [ '11', '11.0.3', '15', '15.0.5'] name: Test Java ${{ matrix.Java }}, ${{ matrix.os }} steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} From 64e0ab8832930c650f308fc21022e2a7b9b8f40a Mon Sep 17 00:00:00 2001 From: jmacxx <47253594+jmacxx@users.noreply.github.com> Date: Mon, 19 Dec 2022 09:14:15 -0600 Subject: [PATCH 30/89] Base initial limit from users max past trade size if applicable. --- .../core/trade/ClosedTradableManager.java | 20 +++++++++++++++++++ .../resources/i18n/displayStrings.properties | 3 ++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/trade/ClosedTradableManager.java b/core/src/main/java/bisq/core/trade/ClosedTradableManager.java index ab53346223..284a75f862 100644 --- a/core/src/main/java/bisq/core/trade/ClosedTradableManager.java +++ b/core/src/main/java/bisq/core/trade/ClosedTradableManager.java @@ -53,6 +53,7 @@ import javafx.collections.ObservableList; import java.time.Instant; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -127,6 +128,7 @@ public class ClosedTradableManager implements PersistedDataHost { public void onAllServicesInitialized() { cleanupMailboxMessagesService.handleTrades(getClosedTrades()); maybeClearSensitiveData(); + maybeIncreaseTradeLimit(); } public void add(Tradable tradable) { @@ -172,6 +174,24 @@ public class ClosedTradableManager implements PersistedDataHost { return closedTradables.stream().filter(e -> e.getId().equals(id)).findFirst(); } + // if user has closed trades of greater size to the default trade limit and has never customized their + // trade limit, then set the limit to the largest amount traded previously. + public void maybeIncreaseTradeLimit() { + if (!preferences.isUserHasRaisedTradeLimit()) { + Optional maxTradeSize = closedTradables.stream() + .filter(e -> e instanceof Trade) + .map(e -> (Trade) e) + .max(Comparator.comparing(Trade::getAmountAsLong)); + maxTradeSize.ifPresent(trade -> { + if (trade.getAmountAsLong() > preferences.getUserDefinedTradeLimit()) { + log.info("Increasing user trade limit to size of max completed trade: {}", trade.getAmount()); + preferences.setUserDefinedTradeLimit(trade.getAmountAsLong()); + preferences.setUserHasRaisedTradeLimit(true); + } + }); + } + } + public void maybeClearSensitiveData() { log.info("checking closed trades eligibility for having sensitive data cleared"); closedTradables.stream() diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 3896e386d6..570ffe7db0 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -3735,9 +3735,10 @@ payment.limits.info=Please be aware that all bank transfers carry a certain amou See more details on the wiki [HYPERLINK:https://bisq.wiki/Account_limits]. # suppress inspection "UnusedProperty" payment.limits.info.withSigning=To limit chargeback risk, Bisq sets per-trade limits for this payment account type based \ - on the following 2 factors:\n\n\ + on the following factors:\n\n\ 1. General chargeback risk for the payment method\n\ 2. Account signing status\n\ + 3. User-defined trade limit\n\ \n\ This payment account is not yet signed, so it is limited to buying {0} per trade. \ After signing, buy limits will increase as follows:\n\ From a726d826c8f6f25101654988a0a1385ef01c1a2f Mon Sep 17 00:00:00 2001 From: Christoph Atteneder Date: Tue, 20 Dec 2022 11:29:14 +0100 Subject: [PATCH 31/89] Not use additional `-%d` in name format because of refactoring --- core/src/main/java/bisq/core/crypto/ScryptUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/crypto/ScryptUtil.java b/core/src/main/java/bisq/core/crypto/ScryptUtil.java index d3d7c2c8ac..f36c3c245f 100644 --- a/core/src/main/java/bisq/core/crypto/ScryptUtil.java +++ b/core/src/main/java/bisq/core/crypto/ScryptUtil.java @@ -49,7 +49,7 @@ public class ScryptUtil { } public static void deriveKeyWithScrypt(KeyCrypterScrypt keyCrypterScrypt, String password, DeriveKeyResultHandler resultHandler) { - Utilities.getThreadPoolExecutor("ScryptUtil:deriveKeyWithScrypt-%d", 1, 2, 5L).submit(() -> { + Utilities.getThreadPoolExecutor("ScryptUtil:deriveKeyWithScrypt", 1, 2, 5L).submit(() -> { try { log.debug("Doing key derivation"); long start = System.currentTimeMillis(); From 9e3b28bf0f703612a819dac56c77a60bd98b8505 Mon Sep 17 00:00:00 2001 From: Christoph Atteneder Date: Tue, 20 Dec 2022 13:32:27 +0100 Subject: [PATCH 32/89] Add back distribution tar generation for daemon and cli This is required in finalize.sh script for creating the release bundle --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index 8d6589243a..c0c0d04d37 100644 --- a/build.gradle +++ b/build.gradle @@ -324,6 +324,8 @@ configure(project(':core')) { } configure(project(':cli')) { + distTar.enabled = true + mainClassName = 'bisq.cli.CliMain' dependencies { @@ -486,6 +488,8 @@ configure(project(':statsnode')) { } configure(project(':daemon')) { + distTar.enabled = true + mainClassName = 'bisq.daemon.app.BisqDaemonMain' dependencies { From e1b332c32dc2b052bb2eb397649776435f9352ef Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Tue, 20 Dec 2022 12:55:57 -0500 Subject: [PATCH 33/89] Remove price node and seed nodes of @miker and @mrosseel Signed-off-by: HenrikJannsen --- core/src/main/java/bisq/core/provider/ProvidersRepository.java | 1 - core/src/main/resources/btc_mainnet.seednodes | 2 -- core/src/test/resources/mainnet.seednodes | 3 --- 3 files changed, 6 deletions(-) diff --git a/core/src/main/java/bisq/core/provider/ProvidersRepository.java b/core/src/main/java/bisq/core/provider/ProvidersRepository.java index f176e5a59a..117e6fcf5e 100644 --- a/core/src/main/java/bisq/core/provider/ProvidersRepository.java +++ b/core/src/main/java/bisq/core/provider/ProvidersRepository.java @@ -38,7 +38,6 @@ public class ProvidersRepository { public static final List DEFAULT_NODES = Arrays.asList( "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/", // @wiz "http://emzypricpidesmyqg2hc6dkwitqzaxrqnpkdg3ae2wef5znncu2ambqd.onion/", // @emzy - "http://aprcndeiwdrkbf4fq7iozxbd27dl72oeo76n7zmjwdi4z34agdrnheyd.onion/", // @mrosseel "http://devinpndvdwll4wiqcyq5e7itezmarg7rzicrvf6brzkwxdm374kmmyd.onion/", // @devinbileck "http://ro7nv73awqs3ga2qtqeqawrjpbxwarsazznszvr6whv7tes5ehffopid.onion/" // @alexej996 ); diff --git a/core/src/main/resources/btc_mainnet.seednodes b/core/src/main/resources/btc_mainnet.seednodes index 5c6cce1b84..8fa70fdbeb 100644 --- a/core/src/main/resources/btc_mainnet.seednodes +++ b/core/src/main/resources/btc_mainnet.seednodes @@ -8,8 +8,6 @@ devinsn3xuzxhj6pmammrxpydhwwmwp75qkksedo5dn2tlmu7jggo7id.onion:8000 (@devinbilec sn3emzy56u3mxzsr4geysc52feoq5qt7ja56km6gygwnszkshunn2sid.onion:8000 (@emzy) sn4emzywye3dhjouv7jig677qepg7fnusjidw74fbwneieruhmi7fuyd.onion:8000 (@emzy) sn5emzyvxuildv34n6jewfp2zeota4aq63fsl5yyilnvksezr3htveqd.onion:8000 (@emzy) -sn3bsq3evqkpshdmc3sbdxafkhfnk7ctop44jsxbxyys5ridsaw5abyd.onion:8000 (@miker) -sn4bsqpc7eb2ntvpsycxbzqt6fre72l4krp2fl5svphfh2eusrqtq3qd.onion:8000 (@miker) jmacxxto7g7welbgcjwfpquzqaehmvp5bf6mqlz5nciho2sc6v7hbyid.onion:8000 (@jmacxx) jmacx22rplg5ckci3s6xvxuuerycwve72wsote4o7suvryd435z3glid.onion:8000 (@jmacxx) jmacx3kp7iku7eod6s4sasvxtdknakbsw2l2us5tyrk3q2634i222zid.onion:8000 (@jmacxx) diff --git a/core/src/test/resources/mainnet.seednodes b/core/src/test/resources/mainnet.seednodes index c3cbdeaefa..8823f09947 100644 --- a/core/src/test/resources/mainnet.seednodes +++ b/core/src/test/resources/mainnet.seednodes @@ -5,6 +5,3 @@ devinsn3xuzxhj6pmammrxpydhwwmwp75qkksedo5dn2tlmu7jggo7id.onion:8000 (@devinbilec sn3emzy56u3mxzsr4geysc52feoq5qt7ja56km6gygwnszkshunn2sid.onion:8000 (@emzy) sn4emzywye3dhjouv7jig677qepg7fnusjidw74fbwneieruhmi7fuyd.onion:8000 (@emzy) sn5emzyvxuildv34n6jewfp2zeota4aq63fsl5yyilnvksezr3htveqd.onion:8000 (@emzy) -sn2bisqad7ncazupgbd3dcedqh5ptirgwofw63djwpdtftwhddo75oid.onion:8000 (@miker) -sn3bsq3evqkpshdmc3sbdxafkhfnk7ctop44jsxbxyys5ridsaw5abyd.onion:8000 (@miker) -sn4bsqpc7eb2ntvpsycxbzqt6fre72l4krp2fl5svphfh2eusrqtq3qd.onion:8000 (@miker) From 59073274fc484cec8be4c5beae0295be9c4b8838 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 21 Dec 2022 19:53:51 -0500 Subject: [PATCH 34/89] Increase queue capacity from 10 to 30. https://github.com/bisq-network/bisq/issues/6480 reported reaching the limit with the current settings. Signed-off-by: HenrikJannsen --- p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java index d7f1da2f85..e90fb1513b 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java +++ b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java @@ -103,12 +103,12 @@ public abstract class NetworkNode implements MessageListener { connectionExecutor = Utilities.getListeningExecutorService("NetworkNode.connection", maxConnections * 2, maxConnections * 3, - 10, + 30, 60); sendMessageExecutor = Utilities.getListeningExecutorService("NetworkNode.sendMessage", maxConnections * 2, maxConnections * 3, - 10, + 30, 60); serverExecutor = Utilities.getSingleThreadExecutor("NetworkNode.server-" + servicePort); } From bb1742958855977c8f41df5bcec983778db394d4 Mon Sep 17 00:00:00 2001 From: jmacxx <47253594+jmacxx@users.noreply.github.com> Date: Fri, 23 Dec 2022 14:49:14 -0600 Subject: [PATCH 35/89] Fix problem causing multiple offers with same ID. --- .../java/bisq/desktop/main/offer/bisq_v1/MutableOfferView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/bisq/desktop/main/offer/bisq_v1/MutableOfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/bisq_v1/MutableOfferView.java index ebdcbd437b..ae0a2471b7 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/bisq_v1/MutableOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/bisq_v1/MutableOfferView.java @@ -296,7 +296,7 @@ public abstract class MutableOfferView> exten /////////////////////////////////////////////////////////////////////////////////////////// public void onTabSelected(boolean isSelected) { - if (isSelected && !model.getDataModel().isTabSelected) { + if (isSelected) { doActivate(); } else { deactivate(); From 72b55f429a6c8040eab9b211a77820d804284068 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Mon, 26 Dec 2022 12:21:20 -0500 Subject: [PATCH 36/89] Move log before super handler to maintain correct order of logs Signed-off-by: HenrikJannsen --- .../java/bisq/core/trade/protocol/bisq_v1/SellerProtocol.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/SellerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/SellerProtocol.java index a3e4fd90d6..46a4a74fd1 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/SellerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/SellerProtocol.java @@ -167,11 +167,11 @@ public abstract class SellerProtocol extends DisputeProtocol { @Override protected void onTradeMessage(TradeMessage message, NodeAddress peer) { - super.onTradeMessage(message, peer); - log.info("Received {} from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peer, message.getTradeId(), message.getUid()); + super.onTradeMessage(message, peer); + if (message instanceof DelayedPayoutTxSignatureResponse) { handle((DelayedPayoutTxSignatureResponse) message, peer); } else if (message instanceof ShareBuyerPaymentAccountMessage) { From 3f4a0692025e9b5be33e52980446e5dba61f56e4 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Mon, 26 Dec 2022 12:29:33 -0500 Subject: [PATCH 37/89] Add reset method to trade protocol. Add map for maintaining pending protocols by offerID. Fixes a bug when the same taker got an early failure in a take offer attempt before the maker removes the offer and the taker did not persist the failed trade, thus the protection to not be permitted to take the same offer after a failure does not prevent another take offer attempt of the same taker. In such a case the maker has the old pending protocol listening for the trade messages and both the old and new protocol instance process those messages. In case the model data has changes (e.g. diff. inputs) this can cause a failed trade. We do not remove the message listeners on failures to tolerate minor failures which can be recovered by resend routines. There could be more solid fixes for that issue but this approach seems to carry less risks. --- .../java/bisq/core/trade/TradeManager.java | 31 ++++++++++++++++--- .../core/trade/protocol/TradeProtocol.java | 6 ++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index 0384187bf1..507e1934e8 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -156,7 +156,18 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi private final Provider provider; private final ClockWatcher clockWatcher; - private final Map tradeProtocolByTradeId = new HashMap<>(); + // We use uid for that map not the trade ID + private final Map tradeProtocolByTradeUid = new HashMap<>(); + + // We maintain a map with trade (offer) ID to reset a pending trade protocol for the same offer. + // Pending trade protocol could happen in edge cases when an early error did not cause a removal of the + // offer and the same peer takes the offer later again. Usually it is prevented for the taker to take again after a + // failure but that is only based on failed trades state and it can be that either the taker deletes the failed trades + // file or it was not persisted. Such rare cases could lead to a pending protocol and when taker takes again the + // offer the message listener from the old pending protocol gets invoked and processes the messages based on + // potentially outdated model data (e.g. old inputs). + private final Map pendingTradeProtocolByTradeId = new HashMap<>(); + private final PersistenceManager> persistenceManager; private final TradableList tradableList = new TradableList<>(); @Getter @@ -408,15 +419,20 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi public TradeProtocol getTradeProtocol(TradeModel trade) { String uid = trade.getUid(); - if (tradeProtocolByTradeId.containsKey(uid)) { - return tradeProtocolByTradeId.get(uid); + if (tradeProtocolByTradeUid.containsKey(uid)) { + return tradeProtocolByTradeUid.get(uid); } else { TradeProtocol tradeProtocol = TradeProtocolFactory.getNewTradeProtocol(trade); - TradeProtocol prev = tradeProtocolByTradeId.put(uid, tradeProtocol); + TradeProtocol prev = tradeProtocolByTradeUid.put(uid, tradeProtocol); if (prev != null) { log.error("We had already an entry with uid {}", trade.getUid()); } + TradeProtocol pending = pendingTradeProtocolByTradeId.put(trade.getId(), tradeProtocol); + if (pending != null) { + pending.reset(); + } + return tradeProtocol; } } @@ -618,7 +634,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi private TradeProtocol createTradeProtocol(TradeModel tradeModel) { TradeProtocol tradeProtocol = TradeProtocolFactory.getNewTradeProtocol(tradeModel); - TradeProtocol prev = tradeProtocolByTradeId.put(tradeModel.getUid(), tradeProtocol); + TradeProtocol prev = tradeProtocolByTradeUid.put(tradeModel.getUid(), tradeProtocol); if (prev != null) { log.error("We had already an entry with uid {}", tradeModel.getUid()); } @@ -626,6 +642,11 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi tradableList.add((Trade) tradeModel); } + TradeProtocol pending = pendingTradeProtocolByTradeId.put(tradeModel.getId(), tradeProtocol); + if (pending != null) { + pending.reset(); + } + // For BsqTrades we only store the trade at completion return tradeProtocol; diff --git a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java index 549439e61b..933ba70ceb 100644 --- a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java @@ -96,6 +96,12 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D cleanup(); } + // Resets a potentially pending protocol + public void reset() { + tradeModel.setErrorMessage("Outdated pending protocol got reset."); + protocolModel.getP2PService().removeDecryptedDirectMessageListener(this); + } + protected void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress) { log.info("Received {} as MailboxMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getTradeId(), message.getUid()); From e0f4aa281a93dc15ff10c994422b9851606e95a5 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Tue, 27 Dec 2022 17:11:38 -0500 Subject: [PATCH 38/89] Catch RejectedExecutionException at UncaughtExceptionHandler and log error instead calling the uncaughtExceptionHandler Signed-off-by: HenrikJannsen --- common/src/main/java/bisq/common/setup/CommonSetup.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/bisq/common/setup/CommonSetup.java b/common/src/main/java/bisq/common/setup/CommonSetup.java index 20d99a22c3..ae92f30af5 100644 --- a/common/src/main/java/bisq/common/setup/CommonSetup.java +++ b/common/src/main/java/bisq/common/setup/CommonSetup.java @@ -35,6 +35,7 @@ import java.net.URISyntaxException; import java.nio.file.Paths; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import ch.qos.logback.classic.Level; @@ -72,13 +73,14 @@ public class CommonSetup { public static void setupUncaughtExceptionHandler(UncaughtExceptionHandler uncaughtExceptionHandler) { Thread.UncaughtExceptionHandler handler = (thread, throwable) -> { - // Might come from another thread if (throwable.getCause() != null && throwable.getCause().getCause() != null && throwable.getCause().getCause() instanceof BlockStoreException) { - log.error(throwable.getMessage()); + log.error("Uncaught BlockStoreException ", throwable); } else if (throwable instanceof ClassCastException && "sun.awt.image.BufImgSurfaceData cannot be cast to sun.java2d.xr.XRSurfaceData".equals(throwable.getMessage())) { log.warn(throwable.getMessage()); + } else if (throwable instanceof RejectedExecutionException) { + log.error("Uncaught RejectedExecutionException ", throwable); } else if (throwable instanceof UnsupportedOperationException && "The system tray is not supported on the current platform.".equals(throwable.getMessage())) { log.warn(throwable.getMessage()); From 7953e353959148100537903f5bc803269574b874 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Tue, 27 Dec 2022 17:13:13 -0500 Subject: [PATCH 39/89] Add executor parameter to sendMessage Add try/catch to handle RejectedExecutionException Signed-off-by: HenrikJannsen --- .../bisq/network/p2p/network/NetworkNode.java | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java index e90fb1513b..409f01e6a3 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java +++ b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java @@ -50,6 +50,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ExecutorService; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; @@ -140,7 +141,7 @@ public abstract class NetworkNode implements MessageListener { SettableFuture resultFuture = SettableFuture.create(); ListenableFuture future = connectionExecutor.submit(() -> { - Thread.currentThread().setName("NetworkNode:SendMessage-to-" + peersNodeAddress.getFullAddress()); + Thread.currentThread().setName("NetworkNode.connectionExecutor:SendMessage-to-" + peersNodeAddress.getFullAddress()); if (peersNodeAddress.equals(getNodeAddress())) { log.warn("We are sending a message to ourselves"); @@ -288,25 +289,36 @@ public abstract class NetworkNode implements MessageListener { return null; } - public SettableFuture sendMessage(Connection connection, NetworkEnvelope networkEnvelope) { - // connection.sendMessage might take a bit (compression, write to stream), so we use a thread to not block - ListenableFuture future = sendMessageExecutor.submit(() -> { - String id = connection.getPeersNodeAddressOptional().isPresent() ? connection.getPeersNodeAddressOptional().get().getFullAddress() : connection.getUid(); - Thread.currentThread().setName("NetworkNode:SendMessage-to-" + id); - connection.sendMessage(networkEnvelope); - return connection; - }); - SettableFuture resultFuture = SettableFuture.create(); - Futures.addCallback(future, new FutureCallback<>() { - public void onSuccess(Connection connection) { - UserThread.execute(() -> resultFuture.set(connection)); - } + return sendMessage(connection, networkEnvelope, sendMessageExecutor); + } - public void onFailure(@NotNull Throwable throwable) { - UserThread.execute(() -> resultFuture.setException(throwable)); - } - }, MoreExecutors.directExecutor()); + public SettableFuture sendMessage(Connection connection, + NetworkEnvelope networkEnvelope, + ListeningExecutorService executor) { + SettableFuture resultFuture = SettableFuture.create(); + try { + ListenableFuture future = executor.submit(() -> { + String id = connection.getPeersNodeAddressOptional().isPresent() ? connection.getPeersNodeAddressOptional().get().getFullAddress() : connection.getUid(); + Thread.currentThread().setName("NetworkNode:SendMessage-to-" + id); + connection.sendMessage(networkEnvelope); + return connection; + }); + + Futures.addCallback(future, new FutureCallback<>() { + public void onSuccess(Connection connection) { + UserThread.execute(() -> resultFuture.set(connection)); + } + + public void onFailure(@NotNull Throwable throwable) { + UserThread.execute(() -> resultFuture.setException(throwable)); + } + }, MoreExecutors.directExecutor()); + + } catch (RejectedExecutionException exception) { + log.error("RejectedExecutionException at sendMessage: ", exception); + resultFuture.setException(exception); + } return resultFuture; } From d5b65fe23950d75cee949b0621e57c26cba78c70 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Tue, 27 Dec 2022 17:31:07 -0500 Subject: [PATCH 40/89] Reduce keepAliveTime to 30 sec. Signed-off-by: HenrikJannsen --- p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java index 409f01e6a3..f4f6a5a321 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java +++ b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java @@ -105,12 +105,12 @@ public abstract class NetworkNode implements MessageListener { maxConnections * 2, maxConnections * 3, 30, - 60); + 30); sendMessageExecutor = Utilities.getListeningExecutorService("NetworkNode.sendMessage", maxConnections * 2, maxConnections * 3, 30, - 60); + 30); serverExecutor = Utilities.getSingleThreadExecutor("NetworkNode.server-" + servicePort); } From a8a0c0e725ac5bf158197841231fb27de91494af Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Tue, 27 Dec 2022 17:33:39 -0500 Subject: [PATCH 41/89] Add custom thread pool to broadcaster The broadcasting consumes most of the threads but has lower priority than other messages being sent. By separating that thread pool from the common sendMessage executor we can reduce the risk that a burst of broadcasts exhausts the thread pool and might drop send message tasks. Signed-off-by: HenrikJannsen --- .../network/p2p/peers/BroadcastHandler.java | 13 ++++++++---- .../bisq/network/p2p/peers/Broadcaster.java | 21 +++++++++++++++++-- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java index b7b89d2579..5a466a5bee 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java @@ -28,6 +28,7 @@ import bisq.common.UserThread; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; @@ -94,7 +95,9 @@ public class BroadcastHandler implements PeerManager.Listener { // API /////////////////////////////////////////////////////////////////////////////////////////// - public void broadcast(List broadcastRequests, boolean shutDownRequested) { + public void broadcast(List broadcastRequests, + boolean shutDownRequested, + ListeningExecutorService executor) { List confirmedConnections = new ArrayList<>(networkNode.getConfirmedConnections()); Collections.shuffle(confirmedConnections); @@ -153,7 +156,7 @@ public class BroadcastHandler implements PeerManager.Listener { return; } - sendToPeer(connection, broadcastRequestsForConnection); + sendToPeer(connection, broadcastRequestsForConnection, executor); }, minDelay, maxDelay, TimeUnit.MILLISECONDS); } } @@ -235,10 +238,12 @@ public class BroadcastHandler implements PeerManager.Listener { .collect(Collectors.toList()); } - private void sendToPeer(Connection connection, List broadcastRequestsForConnection) { + private void sendToPeer(Connection connection, + List broadcastRequestsForConnection, + ListeningExecutorService executor) { // Can be BundleOfEnvelopes or a single BroadcastMessage BroadcastMessage broadcastMessage = getMessage(broadcastRequestsForConnection); - SettableFuture future = networkNode.sendMessage(connection, broadcastMessage); + SettableFuture future = networkNode.sendMessage(connection, broadcastMessage, executor); Futures.addCallback(future, new FutureCallback<>() { @Override diff --git a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java index 2ddc1d8e79..f327a31afd 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java @@ -23,13 +23,20 @@ import bisq.network.p2p.storage.messages.BroadcastMessage; import bisq.common.Timer; import bisq.common.UserThread; +import bisq.common.config.Config; +import bisq.common.util.Utilities; import javax.inject.Inject; +import javax.inject.Named; + +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -49,6 +56,7 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { private Timer timer; private boolean shutDownRequested; private Runnable shutDownResultHandler; + private final ListeningExecutorService executor; /////////////////////////////////////////////////////////////////////////////////////////// @@ -56,9 +64,18 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public Broadcaster(NetworkNode networkNode, PeerManager peerManager) { + public Broadcaster(NetworkNode networkNode, + PeerManager peerManager, + @Named(Config.MAX_CONNECTIONS) int maxConnections) { this.networkNode = networkNode; this.peerManager = peerManager; + + ThreadPoolExecutor threadPoolExecutor = Utilities.getThreadPoolExecutor("Broadcaster", + maxConnections, + maxConnections * 2, + 30, + 30); + executor = MoreExecutors.listeningDecorator(threadPoolExecutor); } public void shutDown(Runnable resultHandler) { @@ -119,7 +136,7 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { broadcastRequests.stream().map(e -> e.getMessage().getClass().getSimpleName()).collect(Collectors.toList())); BroadcastHandler broadcastHandler = new BroadcastHandler(networkNode, peerManager, this); broadcastHandlers.add(broadcastHandler); - broadcastHandler.broadcast(new ArrayList<>(broadcastRequests), shutDownRequested); + broadcastHandler.broadcast(new ArrayList<>(broadcastRequests), shutDownRequested, executor); broadcastRequests.clear(); if (timer != null) { From c3acd5fb4a0ab003e2a49d0a27c4ff9177fab193 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 16 Dec 2022 17:48:06 -0500 Subject: [PATCH 42/89] Handle OutOfMemoryError to cause a shutdown at seed node --- .../main/java/bisq/common/setup/CommonSetup.java | 5 +++++ .../src/main/java/bisq/seednode/SeedNodeMain.java | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/common/src/main/java/bisq/common/setup/CommonSetup.java b/common/src/main/java/bisq/common/setup/CommonSetup.java index ae92f30af5..d46d933433 100644 --- a/common/src/main/java/bisq/common/setup/CommonSetup.java +++ b/common/src/main/java/bisq/common/setup/CommonSetup.java @@ -76,6 +76,11 @@ public class CommonSetup { if (throwable.getCause() != null && throwable.getCause().getCause() != null && throwable.getCause().getCause() instanceof BlockStoreException) { log.error("Uncaught BlockStoreException ", throwable); + } else if (throwable instanceof OutOfMemoryError) { + Profiler.printSystemLoad(); + log.error("OutOfMemoryError occurred. We shut down.", throwable); + // Leave it to the handleUncaughtException to shut down or not. + UserThread.execute(() -> uncaughtExceptionHandler.handleUncaughtException(throwable, false)); } else if (throwable instanceof ClassCastException && "sun.awt.image.BufImgSurfaceData cannot be cast to sun.java2d.xr.XRSurfaceData".equals(throwable.getMessage())) { log.warn(throwable.getMessage()); diff --git a/seednode/src/main/java/bisq/seednode/SeedNodeMain.java b/seednode/src/main/java/bisq/seednode/SeedNodeMain.java index 3bfc2c30e8..81f0e28f80 100644 --- a/seednode/src/main/java/bisq/seednode/SeedNodeMain.java +++ b/seednode/src/main/java/bisq/seednode/SeedNodeMain.java @@ -95,6 +95,19 @@ public class SeedNodeMain extends ExecutableForAppWithP2p { } + /////////////////////////////////////////////////////////////////////////////////////////// + // UncaughtExceptionHandler implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void handleUncaughtException(Throwable throwable, boolean doShutDown) { + if (throwable instanceof OutOfMemoryError || doShutDown) { + log.error("We got an OutOfMemoryError and shut down"); + gracefulShutDown(() -> log.info("gracefulShutDown complete")); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// // We continue with a series of synchronous execution tasks /////////////////////////////////////////////////////////////////////////////////////////// From 1030f891b9c56fdaefadb0156f97919825dab512 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 16 Dec 2022 17:48:58 -0500 Subject: [PATCH 43/89] Improve/cleanup logs. Only log lostAllConnections after numOnConnections > 2 to avoid logs at startup Signed-off-by: HenrikJannsen --- .../java/bisq/network/p2p/network/Connection.java | 12 ++++++------ .../java/bisq/network/p2p/peers/PeerManager.java | 8 ++++++-- .../p2p/peers/getdata/GetDataRequestHandler.java | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/network/Connection.java b/p2p/src/main/java/bisq/network/p2p/network/Connection.java index be0a3390d6..049c690e04 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/Connection.java +++ b/p2p/src/main/java/bisq/network/p2p/network/Connection.java @@ -595,7 +595,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { public boolean reportInvalidRequest(RuleViolation ruleViolation) { - log.warn("We got reported the ruleViolation {} at connection {}", ruleViolation, this); + log.info("We got reported the ruleViolation {} at connection with address{} and uid {}", ruleViolation, this.getPeersNodeAddressProperty(), this.getUid()); int numRuleViolations; numRuleViolations = ruleViolations.getOrDefault(ruleViolation, 0); @@ -603,11 +603,11 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { ruleViolations.put(ruleViolation, numRuleViolations); if (numRuleViolations >= ruleViolation.maxTolerance) { - log.warn("We close connection as we received too many corrupt requests.\n" + - "numRuleViolations={}\n\t" + - "corruptRequest={}\n\t" + - "corruptRequests={}\n\t" + - "connection={}", numRuleViolations, ruleViolation, ruleViolations, this); + log.warn("We close connection as we received too many corrupt requests. " + + "numRuleViolations={} " + + "corruptRequest={} " + + "corruptRequests={} " + + "connection with address{} and uid {}", numRuleViolations, ruleViolation, ruleViolations, this.getPeersNodeAddressProperty(), this.getUid()); this.ruleViolation = ruleViolation; if (ruleViolation == RuleViolation.PEER_BANNED) { log.warn("We close connection due RuleViolation.PEER_BANNED. peersNodeAddress={}", getPeersNodeAddressOptional()); diff --git a/p2p/src/main/java/bisq/network/p2p/peers/PeerManager.java b/p2p/src/main/java/bisq/network/p2p/peers/PeerManager.java index ebc581bbe2..7d9e649b89 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/PeerManager.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/PeerManager.java @@ -84,6 +84,7 @@ public final class PeerManager implements ConnectionListener, PersistedDataHost private static final boolean PRINT_REPORTED_PEERS_DETAILS = true; private Timer printStatisticsTimer; private boolean shutDownRequested; + private int numOnConnections; /////////////////////////////////////////////////////////////////////////////////////////// @@ -216,6 +217,8 @@ public final class PeerManager implements ConnectionListener, PersistedDataHost doHouseKeeping(); + numOnConnections++; + if (lostAllConnections) { lostAllConnections = false; stopped = false; @@ -238,7 +241,8 @@ public final class PeerManager implements ConnectionListener, PersistedDataHost boolean previousLostAllConnections = lostAllConnections; lostAllConnections = networkNode.getAllConnections().isEmpty(); - if (lostAllConnections) { + // At start-up we ignore if we would lose a connection and would fall back to no connections + if (lostAllConnections && numOnConnections > 2) { stopped = true; if (!shutDownRequested) { @@ -562,7 +566,7 @@ public final class PeerManager implements ConnectionListener, PersistedDataHost if (!candidates.isEmpty()) { Connection connection = candidates.remove(0); - log.info("checkMaxConnections: Num candidates for shut down={}. We close oldest connection to peer {}", + log.info("checkMaxConnections: Num candidates (inbound/peer) for shut down={}. We close oldest connection to peer {}", candidates.size(), connection.getPeersNodeAddressOptional()); if (!connection.isStopped()) { connection.shutDown(CloseConnectionReason.TOO_MANY_CONNECTIONS_OPEN, diff --git a/p2p/src/main/java/bisq/network/p2p/peers/getdata/GetDataRequestHandler.java b/p2p/src/main/java/bisq/network/p2p/peers/getdata/GetDataRequestHandler.java index 02aab8e164..e512ecb7c5 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/getdata/GetDataRequestHandler.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/getdata/GetDataRequestHandler.java @@ -136,7 +136,7 @@ public class GetDataRequestHandler { @Override public void onFailure(@NotNull Throwable throwable) { if (!stopped) { - String errorMessage = "Sending getDataRequest to " + connection + + String errorMessage = "Sending getDataResponse to " + connection + " failed. That is expected if the peer is offline. getDataResponse=" + getDataResponse + "." + "Exception: " + throwable.getMessage(); handleFault(errorMessage, CloseConnectionReason.SEND_MSG_FAILURE, connection); From d5b4ce275bf1f03e43a00f39dff16c42f9b8490f Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 16 Dec 2022 17:56:01 -0500 Subject: [PATCH 44/89] Add support for listeners when GetDataResponse and GetBlocksResponse are sent. Signed-off-by: HenrikJannsen --- .../BlindVoteStateMonitoringService.java | 5 +++ .../monitoring/DaoStateMonitoringService.java | 5 +++ .../ProposalStateMonitoringService.java | 9 ++++- .../network/StateNetworkService.java | 34 ++++++++++++++++++- .../full/network/FullNodeNetworkService.java | 19 ++++++++++- .../full/network/GetBlocksRequestHandler.java | 4 +-- .../peers/getdata/GetDataRequestHandler.java | 4 +-- .../p2p/peers/getdata/RequestDataManager.java | 19 ++++++++++- 8 files changed, 91 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/monitoring/BlindVoteStateMonitoringService.java b/core/src/main/java/bisq/core/dao/monitoring/BlindVoteStateMonitoringService.java index f803060a53..e6915a0c92 100644 --- a/core/src/main/java/bisq/core/dao/monitoring/BlindVoteStateMonitoringService.java +++ b/core/src/main/java/bisq/core/dao/monitoring/BlindVoteStateMonitoringService.java @@ -25,6 +25,7 @@ import bisq.core.dao.governance.period.PeriodService; import bisq.core.dao.monitoring.model.BlindVoteStateBlock; import bisq.core.dao.monitoring.model.BlindVoteStateHash; import bisq.core.dao.monitoring.network.BlindVoteStateNetworkService; +import bisq.core.dao.monitoring.network.StateNetworkService; import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesRequest; import bisq.core.dao.monitoring.network.messages.NewBlindVoteStateHashMessage; import bisq.core.dao.state.DaoStateListener; @@ -230,6 +231,10 @@ public class BlindVoteStateMonitoringService implements DaoSetupService, DaoStat blindVoteStateNetworkService.requestHashes(genesisTxInfo.getGenesisBlockHeight(), peersAddress); } + public void addResponseListener(StateNetworkService.ResponseListener responseListener) { + blindVoteStateNetworkService.addResponseListener(responseListener); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Listeners diff --git a/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java b/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java index 1d35f3b237..ff6205ec71 100644 --- a/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java +++ b/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java @@ -23,6 +23,7 @@ import bisq.core.dao.monitoring.model.DaoStateHash; import bisq.core.dao.monitoring.model.UtxoMismatch; import bisq.core.dao.monitoring.network.Checkpoint; import bisq.core.dao.monitoring.network.DaoStateNetworkService; +import bisq.core.dao.monitoring.network.StateNetworkService; import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesRequest; import bisq.core.dao.monitoring.network.messages.NewDaoStateHashMessage; import bisq.core.dao.state.DaoStateListener; @@ -289,6 +290,10 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe createSnapshotHandler = handler; } + public void addResponseListener(StateNetworkService.ResponseListener responseListener) { + daoStateNetworkService.addResponseListener(responseListener); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Listeners diff --git a/core/src/main/java/bisq/core/dao/monitoring/ProposalStateMonitoringService.java b/core/src/main/java/bisq/core/dao/monitoring/ProposalStateMonitoringService.java index c28f33a235..a87c1b1518 100644 --- a/core/src/main/java/bisq/core/dao/monitoring/ProposalStateMonitoringService.java +++ b/core/src/main/java/bisq/core/dao/monitoring/ProposalStateMonitoringService.java @@ -24,6 +24,7 @@ import bisq.core.dao.governance.proposal.ProposalService; import bisq.core.dao.monitoring.model.ProposalStateBlock; import bisq.core.dao.monitoring.model.ProposalStateHash; import bisq.core.dao.monitoring.network.ProposalStateNetworkService; +import bisq.core.dao.monitoring.network.StateNetworkService; import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesRequest; import bisq.core.dao.monitoring.network.messages.NewProposalStateHashMessage; import bisq.core.dao.state.DaoStateListener; @@ -232,6 +233,10 @@ public class ProposalStateMonitoringService implements DaoSetupService, DaoState proposalStateNetworkService.requestHashes(genesisTxInfo.getGenesisBlockHeight(), peersAddress); } + public void addResponseListener(StateNetworkService.ResponseListener responseListener) { + proposalStateNetworkService.addResponseListener(responseListener); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Listeners @@ -294,7 +299,9 @@ public class ProposalStateMonitoringService implements DaoSetupService, DaoState return true; } - private boolean processPeersProposalStateHash(ProposalStateHash proposalStateHash, Optional peersNodeAddress, boolean notifyListeners) { + private boolean processPeersProposalStateHash(ProposalStateHash proposalStateHash, + Optional peersNodeAddress, + boolean notifyListeners) { AtomicBoolean changed = new AtomicBoolean(false); AtomicBoolean inConflictWithNonSeedNode = new AtomicBoolean(this.isInConflictWithNonSeedNode); AtomicBoolean inConflictWithSeedNode = new AtomicBoolean(this.isInConflictWithSeedNode); diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/StateNetworkService.java b/core/src/main/java/bisq/core/dao/monitoring/network/StateNetworkService.java index 2df00fc94f..af3e4eedae 100644 --- a/core/src/main/java/bisq/core/dao/monitoring/network/StateNetworkService.java +++ b/core/src/main/java/bisq/core/dao/monitoring/network/StateNetworkService.java @@ -29,10 +29,16 @@ import bisq.network.p2p.network.NetworkNode; import bisq.network.p2p.peers.Broadcaster; import bisq.network.p2p.peers.PeerManager; +import bisq.common.UserThread; import bisq.common.proto.network.NetworkEnvelope; import javax.inject.Inject; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; + import java.util.HashMap; import java.util.List; import java.util.Map; @@ -42,6 +48,8 @@ import java.util.concurrent.CopyOnWriteArrayList; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; + import javax.annotation.Nullable; @Slf4j @@ -59,6 +67,12 @@ public abstract class StateNetworkService stateHashes, Optional peersNodeAddress); } + public interface ResponseListener { + void onSuccess(int serializedSize); + + void onFault(); + } + protected final NetworkNode networkNode; protected final PeerManager peerManager; private final Broadcaster broadcaster; @@ -67,6 +81,7 @@ public abstract class StateNetworkService requestStateHashHandlerMap = new HashMap<>(); private final List> listeners = new CopyOnWriteArrayList<>(); private boolean messageListenerAdded; + private final List responseListeners = new CopyOnWriteArrayList<>(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -145,7 +160,20 @@ public abstract class StateNetworkService future = networkNode.sendMessage(connection, getStateHashesResponse); + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(Connection connection) { + UserThread.execute(() -> responseListeners.forEach(listeners -> listeners.onSuccess(getStateHashesResponse.toProtoMessage().getSerializedSize())) + ); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + UserThread.execute(() -> responseListeners.forEach(StateNetworkService.ResponseListener::onFault) + ); + } + }, MoreExecutors.directExecutor()); } public void requestHashesFromAllConnectedSeedNodes(int fromHeight) { @@ -171,6 +199,10 @@ public abstract class StateNetworkService getBlocksRequestHandlers = new HashMap<>(); private boolean stopped; + private final List responseListeners = new CopyOnWriteArrayList<>(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -107,6 +116,10 @@ public class FullNodeNetworkService implements MessageListener, PeerManager.List broadcaster.broadcast(newBlockBroadcastMessage, networkNode.getNodeAddress()); } + public void addResponseListener(ResponseListener responseListener) { + responseListeners.add(responseListener); + } + /////////////////////////////////////////////////////////////////////////////////////////// // PeerManager.Listener implementation @@ -166,8 +179,10 @@ public class FullNodeNetworkService implements MessageListener, PeerManager.List daoStateService, new GetBlocksRequestHandler.Listener() { @Override - public void onComplete() { + public void onComplete(int serializedSize) { getBlocksRequestHandlers.remove(uid); + + responseListeners.forEach(listener -> listener.onSuccess(serializedSize)); } @Override @@ -179,6 +194,8 @@ public class FullNodeNetworkService implements MessageListener, PeerManager.List if (connection != null) { peerManager.handleConnectionFault(connection); } + + responseListeners.forEach(ResponseListener::onFault); } else { log.warn("We have stopped already. We ignore that getDataRequestHandler.handle.onFault call."); } diff --git a/core/src/main/java/bisq/core/dao/node/full/network/GetBlocksRequestHandler.java b/core/src/main/java/bisq/core/dao/node/full/network/GetBlocksRequestHandler.java index de8fad9f8b..411d49de4f 100644 --- a/core/src/main/java/bisq/core/dao/node/full/network/GetBlocksRequestHandler.java +++ b/core/src/main/java/bisq/core/dao/node/full/network/GetBlocksRequestHandler.java @@ -57,7 +57,7 @@ class GetBlocksRequestHandler { /////////////////////////////////////////////////////////////////////////////////////////// public interface Listener { - void onComplete(); + void onComplete(int serializedSize); void onFault(String errorMessage, Connection connection); } @@ -120,7 +120,7 @@ class GetBlocksRequestHandler { log.info("Send DataResponse to {} succeeded. getBlocksResponse.getBlocks().size()={}", connection.getPeersNodeAddressOptional(), getBlocksResponse.getBlocks().size()); cleanup(); - listener.onComplete(); + listener.onComplete(getBlocksResponse.toProtoNetworkEnvelope().getSerializedSize()); } else { log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call."); } diff --git a/p2p/src/main/java/bisq/network/p2p/peers/getdata/GetDataRequestHandler.java b/p2p/src/main/java/bisq/network/p2p/peers/getdata/GetDataRequestHandler.java index e512ecb7c5..c000449b90 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/getdata/GetDataRequestHandler.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/getdata/GetDataRequestHandler.java @@ -50,7 +50,7 @@ public class GetDataRequestHandler { /////////////////////////////////////////////////////////////////////////////////////////// public interface Listener { - void onComplete(); + void onComplete(int serializedSize); void onFault(String errorMessage, Connection connection); } @@ -126,8 +126,8 @@ public class GetDataRequestHandler { if (!stopped) { log.trace("Send DataResponse to {} succeeded. getDataResponse={}", connection.getPeersNodeAddressOptional(), getDataResponse); + listener.onComplete(getDataResponse.toProtoNetworkEnvelope().getSerializedSize()); cleanup(); - listener.onComplete(); } else { log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call."); } diff --git a/p2p/src/main/java/bisq/network/p2p/peers/getdata/RequestDataManager.java b/p2p/src/main/java/bisq/network/p2p/peers/getdata/RequestDataManager.java index 8225c5cb07..53fd181dd8 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/getdata/RequestDataManager.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/getdata/RequestDataManager.java @@ -43,6 +43,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -63,6 +64,7 @@ public class RequestDataManager implements MessageListener, ConnectionListener, private static int NUM_ADDITIONAL_SEEDS_FOR_UPDATE_REQUEST = 1; private boolean isPreliminaryDataRequest = true; + /////////////////////////////////////////////////////////////////////////////////////////// // Listener /////////////////////////////////////////////////////////////////////////////////////////// @@ -81,6 +83,12 @@ public class RequestDataManager implements MessageListener, ConnectionListener, } } + public interface ResponseListener { + void onSuccess(int serializedSize); + + void onFault(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Class fields @@ -90,6 +98,7 @@ public class RequestDataManager implements MessageListener, ConnectionListener, private final P2PDataStorage dataStorage; private final PeerManager peerManager; private final List seedNodeAddresses; + private final List responseListeners = new CopyOnWriteArrayList<>(); // As we use Guice injection we cannot set the listener in our constructor but the P2PService calls the setListener // in it's constructor so we can guarantee it is not null. @@ -205,6 +214,10 @@ public class RequestDataManager implements MessageListener, ConnectionListener, return nodeAddressOfPreliminaryDataRequest; } + public void addResponseListener(ResponseListener responseListener) { + responseListeners.add(responseListener); + } + /////////////////////////////////////////////////////////////////////////////////////////// // ConnectionListener implementation @@ -276,9 +289,11 @@ public class RequestDataManager implements MessageListener, ConnectionListener, GetDataRequestHandler getDataRequestHandler = new GetDataRequestHandler(networkNode, dataStorage, new GetDataRequestHandler.Listener() { @Override - public void onComplete() { + public void onComplete(int serializedSize) { getDataRequestHandlers.remove(uid); log.trace("requestDataHandshake completed.\n\tConnection={}", connection); + + responseListeners.forEach(listener -> listener.onSuccess(serializedSize)); } @Override @@ -288,6 +303,8 @@ public class RequestDataManager implements MessageListener, ConnectionListener, log.trace("GetDataRequestHandler failed.\n\tConnection={}\n\t" + "ErrorMessage={}", connection, errorMessage); peerManager.handleConnectionFault(connection); + + responseListeners.forEach(ResponseListener::onFault); } else { log.warn("We have stopped already. We ignore that getDataRequestHandler.handle.onFault call."); } From 12e8b468598ee2fc8814c5abe768a656172405ae Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 16 Dec 2022 18:00:24 -0500 Subject: [PATCH 45/89] Report size or faults of GetDataResponse and GetBlocksResponse. Remove Unspecified and use optional instead. Add reporting for data requests and hash requests. Report commit hash only if present. Report messages only if an enum entry is present. Signed-off-by: HenrikJannsen --- .../reporting/DoubleValueReportingItem.java | 23 ++-- .../reporting/LongValueReportingItem.java | 39 ++++-- .../seednode/reporting/ReportingItem.java | 4 +- .../seednode/reporting/ReportingItems.java | 6 +- .../reporting/SeedNodeReportingService.java | 130 ++++++++++++++++-- .../reporting/StringValueReportingItem.java | 24 ++-- 6 files changed, 181 insertions(+), 45 deletions(-) diff --git a/seednode/src/main/java/bisq/seednode/reporting/DoubleValueReportingItem.java b/seednode/src/main/java/bisq/seednode/reporting/DoubleValueReportingItem.java index b4a7486127..d666a82340 100644 --- a/seednode/src/main/java/bisq/seednode/reporting/DoubleValueReportingItem.java +++ b/seednode/src/main/java/bisq/seednode/reporting/DoubleValueReportingItem.java @@ -17,12 +17,14 @@ package bisq.seednode.reporting; +import java.util.Optional; + import lombok.Getter; import lombok.Setter; +import lombok.extern.slf4j.Slf4j; - +@Slf4j public enum DoubleValueReportingItem implements ReportingItem { - Unspecified("", "Unspecified"), sentBytesPerSec("network", "sentBytesPerSec"), receivedBytesPerSec("network", "receivedBytesPerSec"), receivedMessagesPerSec("network", "receivedMessagesPerSec"), @@ -47,16 +49,15 @@ public enum DoubleValueReportingItem implements ReportingItem { return this; } - public static DoubleValueReportingItem from(String key, double value) { - DoubleValueReportingItem item; + public static Optional from(String key, double value) { try { - item = DoubleValueReportingItem.valueOf(key); + DoubleValueReportingItem item = DoubleValueReportingItem.valueOf(key); + item.setValue(value); + return Optional.of(item); } catch (Throwable t) { - item = Unspecified; + log.warn("No enum value with {}", key); + return Optional.empty(); } - - item.setValue(value); - return item; } @Override @@ -66,8 +67,8 @@ public enum DoubleValueReportingItem implements ReportingItem { .build(); } - public static DoubleValueReportingItem fromProto(protobuf.ReportingItem baseProto, - protobuf.DoubleValueReportingItem proto) { + public static Optional fromProto(protobuf.ReportingItem baseProto, + protobuf.DoubleValueReportingItem proto) { return DoubleValueReportingItem.from(baseProto.getKey(), proto.getValue()); } diff --git a/seednode/src/main/java/bisq/seednode/reporting/LongValueReportingItem.java b/seednode/src/main/java/bisq/seednode/reporting/LongValueReportingItem.java index a62a2d381c..59de27e5ac 100644 --- a/seednode/src/main/java/bisq/seednode/reporting/LongValueReportingItem.java +++ b/seednode/src/main/java/bisq/seednode/reporting/LongValueReportingItem.java @@ -17,13 +17,16 @@ package bisq.seednode.reporting; +import java.util.Optional; + import lombok.Getter; import lombok.Setter; +import lombok.extern.slf4j.Slf4j; - +@Slf4j public enum LongValueReportingItem implements ReportingItem { - Unspecified("", "Unspecified"), OfferPayload("data", "OfferPayload"), + BsqSwapOfferPayload("data", "BsqSwapOfferPayload"), MailboxStoragePayload("data", "MailboxStoragePayload"), TradeStatistics3("data", "TradeStatistics3"), AccountAgeWitness("data", "AccountAgeWitness"), @@ -47,6 +50,21 @@ public enum LongValueReportingItem implements ReportingItem { sentBytes("network", "sentBytes"), receivedBytes("network", "receivedBytes"), + PreliminaryGetDataRequest("network", "PreliminaryGetDataRequest"), + GetUpdatedDataRequest("network", "GetUpdatedDataRequest"), + GetBlocksRequest("network", "GetBlocksRequest"), + GetDaoStateHashesRequest("network", "GetDaoStateHashesRequest"), + GetProposalStateHashesRequest("network", "GetProposalStateHashesRequest"), + GetBlindVoteStateHashesRequest("network", "GetBlindVoteStateHashesRequest"), + + GetDataResponse("network", "GetDataResponse"), + GetBlocksResponse("network", "GetBlocksResponse"), + GetDaoStateHashesResponse("network", "GetDaoStateHashesResponse"), + GetProposalStateHashesResponse("network", "GetProposalStateHashesResponse"), + GetBlindVoteStateHashesResponse("network", "GetBlindVoteStateHashesResponse"), + + failedResponseClassName("network", "failedResponseClassName"), + usedMemoryInMB("node", "usedMemoryInMB"), totalMemoryInMB("node", "totalMemoryInMB"), jvmStartTimeInSec("node", "jvmStartTimeInSec"); @@ -69,16 +87,15 @@ public enum LongValueReportingItem implements ReportingItem { return this; } - public static LongValueReportingItem from(String key, long value) { - LongValueReportingItem item; + public static Optional from(String key, long value) { try { - item = LongValueReportingItem.valueOf(key); + LongValueReportingItem item = LongValueReportingItem.valueOf(key); + item.setValue(value); + return Optional.of(item); } catch (Throwable t) { - item = Unspecified; + log.warn("No enum value with {}", key); + return Optional.empty(); } - - item.setValue(value); - return item; } @Override @@ -88,8 +105,8 @@ public enum LongValueReportingItem implements ReportingItem { .build(); } - public static LongValueReportingItem fromProto(protobuf.ReportingItem baseProto, - protobuf.LongValueReportingItem proto) { + public static Optional fromProto(protobuf.ReportingItem baseProto, + protobuf.LongValueReportingItem proto) { return LongValueReportingItem.from(baseProto.getKey(), proto.getValue()); } diff --git a/seednode/src/main/java/bisq/seednode/reporting/ReportingItem.java b/seednode/src/main/java/bisq/seednode/reporting/ReportingItem.java index 828b658542..57e0020aa0 100644 --- a/seednode/src/main/java/bisq/seednode/reporting/ReportingItem.java +++ b/seednode/src/main/java/bisq/seednode/reporting/ReportingItem.java @@ -20,6 +20,8 @@ package bisq.seednode.reporting; import bisq.common.proto.ProtobufferRuntimeException; import bisq.common.proto.network.NetworkPayload; +import java.util.Optional; + public interface ReportingItem extends NetworkPayload { String getKey(); @@ -35,7 +37,7 @@ public interface ReportingItem extends NetworkPayload { protobuf.ReportingItem toProtoMessage(); - static ReportingItem fromProto(protobuf.ReportingItem proto) { + static Optional fromProto(protobuf.ReportingItem proto) { switch (proto.getMessageCase()) { case STRING_VALUE_REPORTING_ITEM: return StringValueReportingItem.fromProto(proto, proto.getStringValueReportingItem()); diff --git a/seednode/src/main/java/bisq/seednode/reporting/ReportingItems.java b/seednode/src/main/java/bisq/seednode/reporting/ReportingItems.java index d5cb51057d..66afbc6b54 100644 --- a/seednode/src/main/java/bisq/seednode/reporting/ReportingItems.java +++ b/seednode/src/main/java/bisq/seednode/reporting/ReportingItems.java @@ -23,6 +23,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.ArrayList; +import java.util.Optional; import java.util.stream.Collectors; import lombok.Getter; @@ -50,7 +51,10 @@ public class ReportingItems extends ArrayList implements NetworkP public static ReportingItems fromProto(protobuf.ReportingItems proto) { ReportingItems reportingItems = new ReportingItems(proto.getAddress()); reportingItems.addAll(proto.getReportingItemList().stream() - .map(ReportingItem::fromProto).collect(Collectors.toList())); + .map(ReportingItem::fromProto) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList())); return reportingItems; } diff --git a/seednode/src/main/java/bisq/seednode/reporting/SeedNodeReportingService.java b/seednode/src/main/java/bisq/seednode/reporting/SeedNodeReportingService.java index de03faecb3..c775d11bd7 100644 --- a/seednode/src/main/java/bisq/seednode/reporting/SeedNodeReportingService.java +++ b/seednode/src/main/java/bisq/seednode/reporting/SeedNodeReportingService.java @@ -24,6 +24,12 @@ import bisq.core.dao.monitoring.ProposalStateMonitoringService; import bisq.core.dao.monitoring.model.BlindVoteStateBlock; import bisq.core.dao.monitoring.model.DaoStateBlock; import bisq.core.dao.monitoring.model.ProposalStateBlock; +import bisq.core.dao.monitoring.network.StateNetworkService; +import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesRequest; +import bisq.core.dao.node.full.network.FullNodeNetworkService; +import bisq.core.dao.node.messages.GetBlocksRequest; import bisq.core.dao.state.DaoStateListener; import bisq.core.dao.state.DaoStateService; @@ -31,6 +37,9 @@ import bisq.network.p2p.P2PService; import bisq.network.p2p.network.NetworkNode; import bisq.network.p2p.network.Statistic; import bisq.network.p2p.peers.PeerManager; +import bisq.network.p2p.peers.getdata.RequestDataManager; +import bisq.network.p2p.peers.getdata.messages.GetUpdatedDataRequest; +import bisq.network.p2p.peers.getdata.messages.PreliminaryGetDataRequest; import bisq.network.p2p.storage.P2PDataStorage; import bisq.network.p2p.storage.payload.ProtectedStorageEntry; @@ -103,6 +112,8 @@ public class SeedNodeReportingService { DaoStateMonitoringService daoStateMonitoringService, ProposalStateMonitoringService proposalStateMonitoringService, BlindVoteStateMonitoringService blindVoteStateMonitoringService, + RequestDataManager requestDataManager, + FullNodeNetworkService fullNodeNetworkService, @Named(Config.MAX_CONNECTIONS) int maxConnections, @Named(Config.SEED_NODE_REPORTING_SERVER_URL) String seedNodeReportingServerUrl) { this.p2PService = p2PService; @@ -142,6 +153,106 @@ public class SeedNodeReportingService { } }; daoFacade.addBsqStateListener(daoStateListener); + + p2PService.getNetworkNode().addMessageListener((networkEnvelope, connection) -> { + if (networkEnvelope instanceof PreliminaryGetDataRequest || + networkEnvelope instanceof GetUpdatedDataRequest || + networkEnvelope instanceof GetBlocksRequest || + networkEnvelope instanceof GetDaoStateHashesRequest || + networkEnvelope instanceof GetProposalStateHashesRequest || + networkEnvelope instanceof GetBlindVoteStateHashesRequest) { + ReportingItems reportingItems = new ReportingItems(getMyAddress()); + int serializedSize = networkEnvelope.toProtoNetworkEnvelope().getSerializedSize(); + String simpleName = networkEnvelope.getClass().getSimpleName(); + try { + LongValueReportingItem reportingItem = LongValueReportingItem.valueOf(simpleName); + reportingItems.add(reportingItem.withValue(serializedSize)); + sendReportingItems(reportingItems); + } catch (Throwable t) { + log.warn("Could not find enum for {}. Error={}", simpleName, t); + } + } + }); + + requestDataManager.addResponseListener(new RequestDataManager.ResponseListener() { + @Override + public void onSuccess(int serializedSize) { + ReportingItems reportingItems = new ReportingItems(getMyAddress()); + reportingItems.add(LongValueReportingItem.GetDataResponse.withValue(serializedSize)); + sendReportingItems(reportingItems); + } + + @Override + public void onFault() { + ReportingItems reportingItems = new ReportingItems(getMyAddress()); + reportingItems.add(LongValueReportingItem.GetDataResponse.withValue(-1)); + sendReportingItems(reportingItems); + } + }); + + fullNodeNetworkService.addResponseListener(new FullNodeNetworkService.ResponseListener() { + @Override + public void onSuccess(int serializedSize) { + ReportingItems reportingItems = new ReportingItems(getMyAddress()); + reportingItems.add(LongValueReportingItem.GetBlocksResponse.withValue(serializedSize)); + sendReportingItems(reportingItems); + } + + @Override + public void onFault() { + ReportingItems reportingItems = new ReportingItems(getMyAddress()); + reportingItems.add(LongValueReportingItem.GetBlocksResponse.withValue(-1)); + sendReportingItems(reportingItems); + } + }); + + daoStateMonitoringService.addResponseListener(new StateNetworkService.ResponseListener() { + @Override + public void onSuccess(int serializedSize) { + ReportingItems reportingItems = new ReportingItems(getMyAddress()); + reportingItems.add(LongValueReportingItem.GetDaoStateHashesResponse.withValue(serializedSize)); + sendReportingItems(reportingItems); + } + + @Override + public void onFault() { + ReportingItems reportingItems = new ReportingItems(getMyAddress()); + reportingItems.add(LongValueReportingItem.GetDaoStateHashesResponse.withValue(-1)); + sendReportingItems(reportingItems); + } + }); + + proposalStateMonitoringService.addResponseListener(new StateNetworkService.ResponseListener() { + @Override + public void onSuccess(int serializedSize) { + ReportingItems reportingItems = new ReportingItems(getMyAddress()); + reportingItems.add(LongValueReportingItem.GetProposalStateHashesResponse.withValue(serializedSize)); + sendReportingItems(reportingItems); + } + + @Override + public void onFault() { + ReportingItems reportingItems = new ReportingItems(getMyAddress()); + reportingItems.add(LongValueReportingItem.GetProposalStateHashesResponse.withValue(-1)); + sendReportingItems(reportingItems); + } + }); + + blindVoteStateMonitoringService.addResponseListener(new StateNetworkService.ResponseListener() { + @Override + public void onSuccess(int serializedSize) { + ReportingItems reportingItems = new ReportingItems(getMyAddress()); + reportingItems.add(LongValueReportingItem.GetBlindVoteStateHashesResponse.withValue(serializedSize)); + sendReportingItems(reportingItems); + } + + @Override + public void onFault() { + ReportingItems reportingItems = new ReportingItems(getMyAddress()); + reportingItems.add(LongValueReportingItem.GetBlindVoteStateHashesResponse.withValue(-1)); + sendReportingItems(reportingItems); + } + }); } public void shutDown() { @@ -213,7 +324,7 @@ public class SeedNodeReportingService { numItemsByType.putIfAbsent(className, 0); numItemsByType.put(className, numItemsByType.get(className) + 1); }); - numItemsByType.forEach((key, value) -> reportingItems.add(LongValueReportingItem.from(key, value))); + numItemsByType.forEach((key, value) -> LongValueReportingItem.from(key, value).ifPresent(reportingItems::add)); // Network reportingItems.add(LongValueReportingItem.numConnections.withValue(networkNode.getAllConnections().size())); @@ -233,16 +344,15 @@ public class SeedNodeReportingService { reportingItems.add(LongValueReportingItem.maxConnections.withValue(maxConnections)); reportingItems.add(StringValueReportingItem.version.withValue(Version.VERSION)); - // If no commit hash is found we use 0 in hex format - String commitHash = Version.findCommitHash().orElse("00"); - reportingItems.add(StringValueReportingItem.commitHash.withValue(commitHash)); + Version.findCommitHash().ifPresent(commitHash -> reportingItems.add(StringValueReportingItem.commitHash.withValue(commitHash))); sendReportingItems(reportingItems); } private void sendReportingItems(ReportingItems reportingItems) { + String truncated = Utilities.toTruncatedString(reportingItems.toString()); try { - log.info("Send report to monitor server: {}", reportingItems.toString()); + log.info("Going to send report to monitor server: {}", truncated); // We send the data as hex encoded protobuf data. We do not use the envelope as it is not part of the p2p system. byte[] protoMessageAsBytes = reportingItems.toProtoMessageAsBytes(); HttpRequest request = HttpRequest.newBuilder() @@ -253,14 +363,16 @@ public class SeedNodeReportingService { httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()).whenComplete((response, throwable) -> { if (throwable != null) { log.warn("Exception at sending reporting data. {}", throwable.getMessage()); - } else if (response.statusCode() != 200) { - log.error("Response error message: {}", response); + } else if (response.statusCode() == 200) { + log.info("Sent successfully report to monitor server with {} items", reportingItems.size()); + } else { + log.warn("Server responded with error. Response={}", response); } }); } catch (RejectedExecutionException t) { - log.warn("Did not send reportingItems {} because of RejectedExecutionException {}", reportingItems, t.toString()); + log.warn("Did not send reportingItems {} because of RejectedExecutionException {}", truncated, t.toString()); } catch (Throwable t) { - log.warn("Did not send reportingItems {} because of exception {}", reportingItems, t.toString()); + log.warn("Did not send reportingItems {} because of exception {}", truncated, t.toString()); } } diff --git a/seednode/src/main/java/bisq/seednode/reporting/StringValueReportingItem.java b/seednode/src/main/java/bisq/seednode/reporting/StringValueReportingItem.java index 65f9edbf1f..e814fb42f6 100644 --- a/seednode/src/main/java/bisq/seednode/reporting/StringValueReportingItem.java +++ b/seednode/src/main/java/bisq/seednode/reporting/StringValueReportingItem.java @@ -17,13 +17,14 @@ package bisq.seednode.reporting; +import java.util.Optional; + import lombok.Getter; import lombok.Setter; +import lombok.extern.slf4j.Slf4j; - +@Slf4j public enum StringValueReportingItem implements ReportingItem { - Unspecified("", "Unspecified"), - daoStateHash("dao", "daoStateHash"), proposalHash("dao", "proposalHash"), blindVoteHash("dao", "blindVoteHash"), @@ -49,16 +50,15 @@ public enum StringValueReportingItem implements ReportingItem { return this; } - public static StringValueReportingItem from(String key, String value) { - StringValueReportingItem item; + public static Optional from(String key, String value) { try { - item = StringValueReportingItem.valueOf(key); + StringValueReportingItem item = StringValueReportingItem.valueOf(key); + item.setValue(value); + return Optional.of(item); } catch (Throwable t) { - item = Unspecified; + log.warn("No enum value with {}", key); + return Optional.empty(); } - - item.setValue(value); - return item; } @Override @@ -73,8 +73,8 @@ public enum StringValueReportingItem implements ReportingItem { .build(); } - public static StringValueReportingItem fromProto(protobuf.ReportingItem baseProto, - protobuf.StringValueReportingItem proto) { + public static Optional fromProto(protobuf.ReportingItem baseProto, + protobuf.StringValueReportingItem proto) { return StringValueReportingItem.from(baseProto.getKey(), proto.getValue()); } From 0b33a12c1974d5ba6720714bee98b1cc39a92775 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 16 Dec 2022 19:32:02 -0500 Subject: [PATCH 46/89] Change visibility, make isShutdownInProgress volatile Signed-off-by: HenrikJannsen --- core/src/main/java/bisq/core/app/BisqExecutable.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/app/BisqExecutable.java b/core/src/main/java/bisq/core/app/BisqExecutable.java index e861d15b51..a92134dceb 100644 --- a/core/src/main/java/bisq/core/app/BisqExecutable.java +++ b/core/src/main/java/bisq/core/app/BisqExecutable.java @@ -74,7 +74,7 @@ public abstract class BisqExecutable implements GracefulShutDownHandler, BisqSet protected Injector injector; protected AppModule module; protected Config config; - private boolean isShutdownInProgress; + protected volatile boolean isShutdownInProgress; private boolean hasDowngraded; public BisqExecutable(String fullName, String scriptName, String appName, String version) { @@ -281,7 +281,7 @@ public abstract class BisqExecutable implements GracefulShutDownHandler, BisqSet } } - private void flushAndExit(ResultHandler resultHandler, int status) { + protected void flushAndExit(ResultHandler resultHandler, int status) { if (!hasDowngraded) { // If user tried to downgrade we do not write the persistable data to avoid data corruption log.info("PersistenceManager flushAllDataToDiskAtShutdown started"); From 7f16e874d262499b739e655199111b60b4b3268d Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 16 Dec 2022 20:49:06 -0500 Subject: [PATCH 47/89] Improve shutdown at dao nodes Signed-off-by: HenrikJannsen --- .../main/java/bisq/core/dao/node/BsqNode.java | 6 ++++++ .../java/bisq/core/dao/node/full/FullNode.java | 4 ++++ .../bisq/core/dao/node/full/RpcService.java | 17 ++++++++++------- .../java/bisq/core/dao/node/lite/LiteNode.java | 3 +++ 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/node/BsqNode.java b/core/src/main/java/bisq/core/dao/node/BsqNode.java index 2e11ce5d12..0bebd702da 100644 --- a/core/src/main/java/bisq/core/dao/node/BsqNode.java +++ b/core/src/main/java/bisq/core/dao/node/BsqNode.java @@ -72,6 +72,7 @@ public abstract class BsqNode implements DaoSetupService { // (not parsed) block. @Getter protected int chainTipHeight; + protected volatile boolean shutdownInProgress; /////////////////////////////////////////////////////////////////////////////////////////// @@ -156,6 +157,7 @@ public abstract class BsqNode implements DaoSetupService { } public void shutDown() { + shutdownInProgress = true; exportJsonFilesService.shutDown(); daoStateSnapshotService.shutDown(); } @@ -200,6 +202,10 @@ public abstract class BsqNode implements DaoSetupService { protected Optional doParseBlock(RawBlock rawBlock) throws RequiredReorgFromSnapshotException { + if (shutdownInProgress) { + return Optional.empty(); + } + // We check if we have a block with that height. If so we return. We do not use the chainHeight as with genesis // height we have no block but chainHeight is initially set to genesis height (bad design ;-( but a bit tricky // to change now as it used in many areas.) diff --git a/core/src/main/java/bisq/core/dao/node/full/FullNode.java b/core/src/main/java/bisq/core/dao/node/full/FullNode.java index 93c18a9a67..469f19b608 100644 --- a/core/src/main/java/bisq/core/dao/node/full/FullNode.java +++ b/core/src/main/java/bisq/core/dao/node/full/FullNode.java @@ -97,6 +97,7 @@ public class FullNode extends BsqNode { public void shutDown() { super.shutDown(); + rpcService.shutDown(); fullNodeNetworkService.shutDown(); } @@ -239,6 +240,9 @@ public class FullNode extends BsqNode { Consumer newBlockHandler, ResultHandler resultHandler, Consumer errorHandler) { + if (shutdownInProgress) { + return; + } rpcService.requestDtoBlock(blockHeight, rawBlock -> { try { diff --git a/core/src/main/java/bisq/core/dao/node/full/RpcService.java b/core/src/main/java/bisq/core/dao/node/full/RpcService.java index d7e557db38..175189ffeb 100644 --- a/core/src/main/java/bisq/core/dao/node/full/RpcService.java +++ b/core/src/main/java/bisq/core/dao/node/full/RpcService.java @@ -92,7 +92,7 @@ public class RpcService { // We could use multiple threads, but then we need to support ordering of results in a queue // Keep that for optimization after measuring performance differences private final ListeningExecutorService executor = Utilities.getSingleThreadListeningExecutor("RpcService"); - private volatile boolean isShutDown; + private volatile boolean shutdownInProgress; private final Set setupResultHandlers = new CopyOnWriteArraySet<>(); private final Set> setupErrorHandlers = new CopyOnWriteArraySet<>(); private volatile boolean setupComplete; @@ -139,14 +139,17 @@ public class RpcService { /////////////////////////////////////////////////////////////////////////////////////////// public void shutDown() { - isShutDown = true; + if (shutdownInProgress) { + return; + } + shutdownInProgress = true; if (daemon != null) { daemon.shutdown(); log.info("daemon shut down"); } // A hard shutdown is justified for the RPC service. - executor.shutdown(); + executor.shutdownNow(); } public void setup(ResultHandler resultHandler, Consumer errorHandler) { @@ -217,7 +220,7 @@ public class RpcService { } }, MoreExecutors.directExecutor()); } catch (Exception e) { - if (!isShutDown || !(e instanceof RejectedExecutionException)) { + if (!shutdownInProgress || !(e instanceof RejectedExecutionException)) { log.warn(e.toString(), e); throw e; } @@ -311,7 +314,7 @@ public class RpcService { } }, MoreExecutors.directExecutor()); } catch (Exception e) { - if (!isShutDown || !(e instanceof RejectedExecutionException)) { + if (!shutdownInProgress || !(e instanceof RejectedExecutionException)) { log.error("Exception at requestChainHeadHeight", e); throw e; } @@ -346,7 +349,7 @@ public class RpcService { }, MoreExecutors.directExecutor()); } catch (Exception e) { log.error("Exception at requestDtoBlock", e); - if (!isShutDown || !(e instanceof RejectedExecutionException)) { + if (!shutdownInProgress || !(e instanceof RejectedExecutionException)) { log.warn(e.toString(), e); throw e; } @@ -381,7 +384,7 @@ public class RpcService { } }, MoreExecutors.directExecutor()); } catch (Exception e) { - if (!isShutDown || !(e instanceof RejectedExecutionException)) { + if (!shutdownInProgress || !(e instanceof RejectedExecutionException)) { log.warn(e.toString(), e); throw e; } diff --git a/core/src/main/java/bisq/core/dao/node/lite/LiteNode.java b/core/src/main/java/bisq/core/dao/node/lite/LiteNode.java index 6d4fa40f3c..68e2769e14 100644 --- a/core/src/main/java/bisq/core/dao/node/lite/LiteNode.java +++ b/core/src/main/java/bisq/core/dao/node/lite/LiteNode.java @@ -248,6 +248,9 @@ public class LiteNode extends BsqNode { } private void runDelayedBatchProcessing(List blocks, Runnable resultHandler) { + if (shutdownInProgress) { + return; + } UserThread.execute(() -> { if (blocks.isEmpty()) { resultHandler.run(); From abbee20284a6befdfc9f4e1c09356743a3252c03 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Mon, 19 Dec 2022 23:39:48 -0500 Subject: [PATCH 48/89] Remove CompletableFutureUtil Signed-off-by: HenrikJannsen --- .../common/util/CompletableFutureUtil.java | 57 ------------------- 1 file changed, 57 deletions(-) delete mode 100644 common/src/main/java/bisq/common/util/CompletableFutureUtil.java diff --git a/common/src/main/java/bisq/common/util/CompletableFutureUtil.java b/common/src/main/java/bisq/common/util/CompletableFutureUtil.java deleted file mode 100644 index 7c885ab4db..0000000000 --- a/common/src/main/java/bisq/common/util/CompletableFutureUtil.java +++ /dev/null @@ -1,57 +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 . - */ - -package bisq.common.util; - -import java.util.Collection; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -//todo -public class CompletableFutureUtil { - public static CompletableFuture> allOf(Collection> collection) { - //noinspection unchecked - return allOf(collection.toArray(new CompletableFuture[0])); - } - - public static CompletableFuture> allOf(Stream> stream) { - return allOf(stream.collect(Collectors.toList())); - } - - public static CompletableFuture> allOf(CompletableFuture... list) { - CompletableFuture> result = CompletableFuture.allOf(list).thenApply(v -> - Stream.of(list) - .map(future -> { - // We want to return the results in list, not the futures. Once allOf call is complete - // we know that all futures have completed (normally, exceptional or cancelled). - // For exceptional and canceled cases we throw an exception. - T res = future.join(); - if (future.isCompletedExceptionally()) { - throw new RuntimeException((future.handle((r, throwable) -> throwable).join())); - } - if (future.isCancelled()) { - throw new RuntimeException("Future got canceled"); - } - return res; - }) - .collect(Collectors.toList()) - ); - return result; - } -} From e1d8d0a6a0355edad54ef2095391e24f3625baa5 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Tue, 20 Dec 2022 19:34:18 -0500 Subject: [PATCH 49/89] Make codacy happy Signed-off-by: HenrikJannsen --- core/src/main/java/bisq/core/dao/node/full/RpcService.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/node/full/RpcService.java b/core/src/main/java/bisq/core/dao/node/full/RpcService.java index 175189ffeb..9f3c999f02 100644 --- a/core/src/main/java/bisq/core/dao/node/full/RpcService.java +++ b/core/src/main/java/bisq/core/dao/node/full/RpcService.java @@ -219,11 +219,14 @@ public class RpcService { }); } }, MoreExecutors.directExecutor()); - } catch (Exception e) { - if (!shutdownInProgress || !(e instanceof RejectedExecutionException)) { + } catch (RejectedExecutionException e) { + if (!shutdownInProgress) { log.warn(e.toString(), e); throw e; } + } catch (Exception e) { + log.warn(e.toString(), e); + throw e; } } From 9a31606fea720b3950458922570c4b175d54c9d6 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 22 Dec 2022 23:02:14 -0500 Subject: [PATCH 50/89] Make codacy happy Signed-off-by: HenrikJannsen --- .../bisq/core/dao/node/full/RpcService.java | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/node/full/RpcService.java b/core/src/main/java/bisq/core/dao/node/full/RpcService.java index 9f3c999f02..2412e4e356 100644 --- a/core/src/main/java/bisq/core/dao/node/full/RpcService.java +++ b/core/src/main/java/bisq/core/dao/node/full/RpcService.java @@ -221,11 +221,11 @@ public class RpcService { }, MoreExecutors.directExecutor()); } catch (RejectedExecutionException e) { if (!shutdownInProgress) { - log.warn(e.toString(), e); + log.error(e.toString(), e); throw e; } } catch (Exception e) { - log.warn(e.toString(), e); + log.error(e.toString(), e); throw e; } } @@ -316,11 +316,14 @@ public class RpcService { UserThread.execute(() -> errorHandler.accept(throwable)); } }, MoreExecutors.directExecutor()); - } catch (Exception e) { - if (!shutdownInProgress || !(e instanceof RejectedExecutionException)) { + } catch (RejectedExecutionException e) { + if (!shutdownInProgress) { log.error("Exception at requestChainHeadHeight", e); throw e; } + } catch (Exception e) { + log.error("Exception at requestChainHeadHeight", e); + throw e; } } @@ -350,12 +353,14 @@ public class RpcService { UserThread.execute(() -> errorHandler.accept(throwable)); } }, MoreExecutors.directExecutor()); - } catch (Exception e) { - log.error("Exception at requestDtoBlock", e); - if (!shutdownInProgress || !(e instanceof RejectedExecutionException)) { - log.warn(e.toString(), e); + } catch (RejectedExecutionException e) { + if (!shutdownInProgress) { + log.error("Exception at requestDtoBlock", e); throw e; } + } catch (Exception e) { + log.error("Exception at requestDtoBlock", e); + throw e; } } @@ -386,11 +391,14 @@ public class RpcService { UserThread.execute(() -> errorHandler.accept(throwable)); } }, MoreExecutors.directExecutor()); - } catch (Exception e) { - if (!shutdownInProgress || !(e instanceof RejectedExecutionException)) { - log.warn(e.toString(), e); + } catch (RejectedExecutionException e) { + if (!shutdownInProgress) { + log.error(e.toString(), e); throw e; } + } catch (Exception e) { + log.error(e.toString(), e); + throw e; } } From 5b38e5b83b6f40de352228ec51d787fa7849a603 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Tue, 27 Dec 2022 18:40:38 -0500 Subject: [PATCH 51/89] Allow to set fullNode if isBmFullNode and rpcDataSet is true even if BurningManService.isActivated() is false. This allows us to run the seed nodes already in BM mode before activation Signed-off-by: HenrikJannsen --- .../burningman/accounting/node/AccountingNodeProvider.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/accounting/node/AccountingNodeProvider.java b/core/src/main/java/bisq/core/dao/burningman/accounting/node/AccountingNodeProvider.java index 0022aa7b77..9efff49e2c 100644 --- a/core/src/main/java/bisq/core/dao/burningman/accounting/node/AccountingNodeProvider.java +++ b/core/src/main/java/bisq/core/dao/burningman/accounting/node/AccountingNodeProvider.java @@ -48,10 +48,10 @@ public class AccountingNodeProvider { && preferences.getRpcPw() != null && !preferences.getRpcPw().isEmpty() && preferences.getBlockNotifyPort() > 0; - if (BurningManService.isActivated()) { - accountingNode = isBmFullNode && rpcDataSet ? fullNode : liteNode; + if (isBmFullNode && rpcDataSet) { + accountingNode = fullNode; } else { - accountingNode = inActiveAccountingNode; + accountingNode = BurningManService.isActivated() ? liteNode : inActiveAccountingNode; } } } From 5ff7be92082410ae3ee63214cbfcb8ed6ed9a619 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 28 Dec 2022 12:09:59 -0500 Subject: [PATCH 52/89] Increase capacity for thread pool queue at ExportJsonFilesService One seed node using the ExportJsonFilesService had RejectedExecutionExceptions as before we had only permitted a thread pool queue capacity of 1. Signed-off-by: HenrikJannsen --- .../core/dao/node/explorer/ExportJsonFilesService.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/node/explorer/ExportJsonFilesService.java b/core/src/main/java/bisq/core/dao/node/explorer/ExportJsonFilesService.java index 1175a26446..8ff811b440 100644 --- a/core/src/main/java/bisq/core/dao/node/explorer/ExportJsonFilesService.java +++ b/core/src/main/java/bisq/core/dao/node/explorer/ExportJsonFilesService.java @@ -53,6 +53,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.ThreadPoolExecutor; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @@ -63,8 +64,7 @@ public class ExportJsonFilesService implements DaoSetupService { private final File storageDir; private final boolean dumpBlockchainData; - private final ListeningExecutorService executor = Utilities.getListeningExecutorService("JsonExporter", - 1, 1, 1200); + private final ListeningExecutorService executor; private JsonFileManager txFileManager, txOutputFileManager, bsqStateFileManager; @Inject @@ -74,6 +74,9 @@ public class ExportJsonFilesService implements DaoSetupService { this.daoStateService = daoStateService; this.storageDir = storageDir; this.dumpBlockchainData = dumpBlockchainData; + + ThreadPoolExecutor threadPoolExecutor = Utilities.getThreadPoolExecutor("JsonExporter", 1, 1, 20, 60); + executor = MoreExecutors.listeningDecorator(threadPoolExecutor); } From fa6beffa49bbb49cdfdbf5288b40803c4bcc5d0a Mon Sep 17 00:00:00 2001 From: jmacxx <47253594+jmacxx@users.noreply.github.com> Date: Thu, 29 Dec 2022 13:57:13 -0600 Subject: [PATCH 53/89] Apply code review changes. --- .../core/provider/mempool/TxValidator.java | 19 +++---------------- .../provider/mempool/TxValidatorTest.java | 2 +- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/bisq/core/provider/mempool/TxValidator.java b/core/src/main/java/bisq/core/provider/mempool/TxValidator.java index 10425dc1b8..0d71fd55f1 100644 --- a/core/src/main/java/bisq/core/provider/mempool/TxValidator.java +++ b/core/src/main/java/bisq/core/provider/mempool/TxValidator.java @@ -61,7 +61,8 @@ public class TxValidator { private final List errorList; private final String txId; private Coin amount; - private Boolean isFeeCurrencyBtc = true; + @Nullable + private Boolean isFeeCurrencyBtc; @Nullable private Long chainHeight; @Setter @@ -70,21 +71,7 @@ public class TxValidator { public TxValidator(DaoStateService daoStateService, String txId, Coin amount, - @Nullable Boolean isFeeCurrencyBtc, - FilterManager filterManager) { - this.daoStateService = daoStateService; - this.txId = txId; - this.amount = amount; - this.isFeeCurrencyBtc = isFeeCurrencyBtc; - this.filterManager = filterManager; - this.errorList = new ArrayList<>(); - this.jsonTxt = ""; - } - - public TxValidator(DaoStateService daoStateService, - String txId, - Coin amount, - @Nullable Boolean isFeeCurrencyBtc, + boolean isFeeCurrencyBtc, long feePaymentBlockHeight, FilterManager filterManager) { this.daoStateService = daoStateService; diff --git a/core/src/test/java/bisq/core/provider/mempool/TxValidatorTest.java b/core/src/test/java/bisq/core/provider/mempool/TxValidatorTest.java index d8e1fcca95..105c3cdb5b 100644 --- a/core/src/test/java/bisq/core/provider/mempool/TxValidatorTest.java +++ b/core/src/test/java/bisq/core/provider/mempool/TxValidatorTest.java @@ -209,7 +209,7 @@ public class TxValidatorTest { knownValuesList.forEach(offerData -> { TxValidator txValidator = createTxValidator(offerData); log.warn("TESTING {}", txValidator.getTxId()); - if (txValidator.getIsFeeCurrencyBtc()) { + if (txValidator.getIsFeeCurrencyBtc() != null && txValidator.getIsFeeCurrencyBtc()) { String jsonTxt = mempoolData.get(txValidator.getTxId()); if (jsonTxt == null || jsonTxt.isEmpty()) { log.warn("{} was not found in the mempool", txValidator.getTxId()); From 8bde140e3d25a26c10aefd22f5a2a3bf1c212f05 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Mon, 2 Jan 2023 17:01:38 -0500 Subject: [PATCH 54/89] Add all addresses of all burningman who have burned BSQ to the list of addresses who are permitted to receive trade fees. Signed-off-by: HenrikJannsen --- .../core/provider/mempool/MempoolService.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/provider/mempool/MempoolService.java b/core/src/main/java/bisq/core/provider/mempool/MempoolService.java index d86b052683..dde464e27c 100644 --- a/core/src/main/java/bisq/core/provider/mempool/MempoolService.java +++ b/core/src/main/java/bisq/core/provider/mempool/MempoolService.java @@ -18,6 +18,7 @@ package bisq.core.provider.mempool; import bisq.core.dao.DaoFacade; +import bisq.core.dao.burningman.BurningManPresentationService; import bisq.core.dao.state.DaoStateService; import bisq.core.filter.FilterManager; import bisq.core.offer.bisq_v1.OfferPayload; @@ -43,8 +44,10 @@ import com.google.common.util.concurrent.SettableFuture; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; +import java.util.stream.Collectors; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -60,6 +63,7 @@ public class MempoolService { private final FilterManager filterManager; private final DaoFacade daoFacade; private final DaoStateService daoStateService; + private final BurningManPresentationService burningManPresentationService; @Getter private int outstandingRequests = 0; @@ -69,13 +73,15 @@ public class MempoolService { Preferences preferences, FilterManager filterManager, DaoFacade daoFacade, - DaoStateService daoStateService) { + DaoStateService daoStateService, + BurningManPresentationService burningManPresentationService) { this.socks5ProxyProvider = socks5ProxyProvider; this.config = config; this.preferences = preferences; this.filterManager = filterManager; this.daoFacade = daoFacade; this.daoStateService = daoStateService; + this.burningManPresentationService = burningManPresentationService; } public void onAllServicesInitialized() { @@ -266,7 +272,17 @@ public class MempoolService { } }); btcFeeReceivers.addAll(daoFacade.getAllDonationAddresses()); - log.debug("Known BTC fee receivers: {}", btcFeeReceivers.toString()); + + // We use all BM who had ever had burned BSQ to avoid if a BM just got "deactivated" due decayed burn amounts + // that it would trigger a failure here. There is still a small risk that new BM used for the trade fee payment + // is not yet visible to the other peer, but that should be very unlikely. + // We also get all addresses related to comp. requests, so this list is still rather long, but much shorter + // than if we would use all addresses of all BM. + Set distributedBMAddresses = burningManPresentationService.getBurningManCandidatesByName().values().stream() + .filter(burningManCandidate -> burningManCandidate.getAccumulatedBurnAmount() > 0) + .flatMap(burningManCandidate -> burningManCandidate.getAllAddresses().stream()) + .collect(Collectors.toSet()); + btcFeeReceivers.addAll(distributedBMAddresses); return btcFeeReceivers; } From 0c4199fef4d0f01b5e16fb481482bd8710f2a4f6 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Tue, 3 Jan 2023 11:31:41 -0500 Subject: [PATCH 55/89] Fix but with calculating the miner fee for the DPT. Add activation date for hotfix. We used all potential BM instead only the ones who have a positive cappedBurnAmountShare. --- .../core/dao/burningman/BurningManService.java | 8 ++++++++ .../DelayedPayoutTxReceiverService.java | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/BurningManService.java b/core/src/main/java/bisq/core/dao/burningman/BurningManService.java index 8fc2a9bc58..c85f8aff18 100644 --- a/core/src/main/java/bisq/core/dao/burningman/BurningManService.java +++ b/core/src/main/java/bisq/core/dao/burningman/BurningManService.java @@ -53,6 +53,7 @@ import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; @@ -207,6 +208,13 @@ public class BurningManService { return daoStateService.getParamValue(Param.RECIPIENT_BTC_ADDRESS, chainHeight); } + Set getActiveBurningManCandidates(int chainHeight) { + return getBurningManCandidatesByName(chainHeight).values().stream() + .filter(burningManCandidate -> burningManCandidate.getCappedBurnAmountShare() > 0) + .filter(candidate -> candidate.getMostRecentAddress().isPresent()) + .collect(Collectors.toSet()); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Private diff --git a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java index 1f3056fec2..f1aba97d9c 100644 --- a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java +++ b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java @@ -25,6 +25,7 @@ import bisq.core.dao.state.model.blockchain.Block; import bisq.common.config.Config; import bisq.common.util.Tuple2; +import bisq.common.util.Utilities; import javax.inject.Inject; import javax.inject.Singleton; @@ -33,6 +34,8 @@ import com.google.common.annotations.VisibleForTesting; import java.util.Collection; import java.util.Comparator; +import java.util.Date; +import java.util.GregorianCalendar; import java.util.List; import java.util.stream.Collectors; @@ -49,6 +52,12 @@ import static com.google.common.base.Preconditions.checkArgument; @Slf4j @Singleton public class DelayedPayoutTxReceiverService implements DaoStateListener { + private static final Date HOTFIX_ACTIVATION_DATE = Utilities.getUTCDate(2023, GregorianCalendar.JANUARY, 10); + + public static boolean isHotfixActivated() { + return new Date().after(HOTFIX_ACTIVATION_DATE); + } + // We don't allow to get further back than 767950 (the block height from Dec. 18th 2022). static final int MIN_SNAPSHOT_HEIGHT = Config.baseCurrencyNetwork().isRegtest() ? 0 : 767950; @@ -113,9 +122,12 @@ public class DelayedPayoutTxReceiverService implements DaoStateListener { public List> getReceivers(int burningManSelectionHeight, long inputAmount, long tradeTxFee) { - checkArgument(burningManSelectionHeight >= MIN_SNAPSHOT_HEIGHT, "Selection height must be >= " + MIN_SNAPSHOT_HEIGHT); - Collection burningManCandidates = burningManService.getBurningManCandidatesByName(burningManSelectionHeight).values(); + Collection burningManCandidates = isHotfixActivated() ? + burningManService.getActiveBurningManCandidates(burningManSelectionHeight) : + burningManService.getBurningManCandidatesByName(burningManSelectionHeight).values(); + + if (burningManCandidates.isEmpty()) { // If there are no compensation requests (e.g. at dev testing) we fall back to the legacy BM return List.of(new Tuple2<>(inputAmount, burningManService.getLegacyBurningManAddress(burningManSelectionHeight))); From a296887a73966c05ce11bae6199fffb0517e0bb7 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Tue, 3 Jan 2023 12:08:12 -0500 Subject: [PATCH 56/89] Use getActiveBurningManCandidates for fee distribution Signed-off-by: HenrikJannsen --- .../core/dao/burningman/BtcFeeReceiverService.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/BtcFeeReceiverService.java b/core/src/main/java/bisq/core/dao/burningman/BtcFeeReceiverService.java index 8e042d7edb..e7f9cde77a 100644 --- a/core/src/main/java/bisq/core/dao/burningman/BtcFeeReceiverService.java +++ b/core/src/main/java/bisq/core/dao/burningman/BtcFeeReceiverService.java @@ -29,7 +29,6 @@ import com.google.common.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Random; import java.util.stream.Collectors; @@ -75,8 +74,8 @@ public class BtcFeeReceiverService implements DaoStateListener { return BurningManPresentationService.LEGACY_BURNING_MAN_BTC_FEES_ADDRESS; } - Map burningManCandidatesByName = burningManService.getBurningManCandidatesByName(currentChainHeight); - if (burningManCandidatesByName.isEmpty()) { + List activeBurningManCandidates = new ArrayList<>(burningManService.getActiveBurningManCandidates(currentChainHeight)); + if (activeBurningManCandidates.isEmpty()) { // If there are no compensation requests (e.g. at dev testing) we fall back to the default address return burningManService.getLegacyBurningManAddress(currentChainHeight); } @@ -86,9 +85,9 @@ public class BtcFeeReceiverService implements DaoStateListener { // cappedBurnAmountShare is a % value represented as double. Smallest supported value is 0.01% -> 0.0001. // By multiplying it with 10000 and using Math.floor we limit the candidate to 0.01%. // Entries with 0 will be ignored in the selection method, so we do not need to filter them out. - List burningManCandidates = new ArrayList<>(burningManCandidatesByName.values()); + // List burningManCandidates = new ArrayList<>(burningManCandidatesByName.values()); int ceiling = 10000; - List amountList = burningManCandidates.stream() + List amountList = activeBurningManCandidates.stream() .map(BurningManCandidate::getCappedBurnAmountShare) .map(cappedBurnAmountShare -> (long) Math.floor(cappedBurnAmountShare * ceiling)) .collect(Collectors.toList()); @@ -99,12 +98,12 @@ public class BtcFeeReceiverService implements DaoStateListener { } int winnerIndex = getRandomIndex(amountList, new Random()); - if (winnerIndex == burningManCandidates.size()) { + if (winnerIndex == activeBurningManCandidates.size()) { // If we have filled up the missing gap to 100% with the legacy BM we would get an index out of bounds of // the burningManCandidates as we added for the legacy BM an entry at the end. return burningManService.getLegacyBurningManAddress(currentChainHeight); } - return burningManCandidates.get(winnerIndex).getMostRecentAddress() + return activeBurningManCandidates.get(winnerIndex).getMostRecentAddress() .orElse(burningManService.getLegacyBurningManAddress(currentChainHeight)); } From 56377d89302cb32ccb86ebfa3da94f68fda59510 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Tue, 3 Jan 2023 17:32:10 -0500 Subject: [PATCH 57/89] Reduce DPT_MIN_REMAINDER_TO_LEGACY_BM from 50k sat to 25k sat. Signed-off-by: HenrikJannsen --- .../core/dao/burningman/DelayedPayoutTxReceiverService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java index f1aba97d9c..661189daf8 100644 --- a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java +++ b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java @@ -69,9 +69,9 @@ public class DelayedPayoutTxReceiverService implements DaoStateListener { // If at DPT there is some leftover amount due to capping of some receivers (burn share is // max. ISSUANCE_BOOST_FACTOR times the issuance share) we send it to legacy BM if it is larger // than DPT_MIN_REMAINDER_TO_LEGACY_BM, otherwise we spend it as miner fee. - // 50000 sat is about 10 USD @ 20k price. We use a rather high value as we want to avoid that the legacy BM + // 25000 sat is about 5 USD @ 20k price. We use a rather high value as we want to avoid that the legacy BM // gets still payouts. - private static final long DPT_MIN_REMAINDER_TO_LEGACY_BM = 50000; + private static final long DPT_MIN_REMAINDER_TO_LEGACY_BM = 25000; // Min. fee rate for DPT. If fee rate used at take offer time was higher we use that. // We prefer a rather high fee rate to not risk that the DPT gets stuck if required fee rate would From b7d1a9da2273b20be0ae22b3bf26a58d7e31ce80 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Tue, 3 Jan 2023 19:58:29 -0500 Subject: [PATCH 58/89] Increase thread pool size at Broadcaster Signed-off-by: HenrikJannsen --- p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java index f327a31afd..dee52c2f87 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java @@ -71,8 +71,8 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { this.peerManager = peerManager; ThreadPoolExecutor threadPoolExecutor = Utilities.getThreadPoolExecutor("Broadcaster", - maxConnections, maxConnections * 2, + maxConnections * 3, 30, 30); executor = MoreExecutors.listeningDecorator(threadPoolExecutor); From 41fb5e464ca37cf0dd42712c8141c4318aaea704 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 4 Jan 2023 12:58:22 -0500 Subject: [PATCH 59/89] Use AtomicBoolean for stopped and timeoutTriggered Signed-off-by: HenrikJannsen --- .../network/p2p/peers/BroadcastHandler.java | 58 ++++++++++--------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java index 5a466a5bee..7f33f233e0 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java @@ -37,6 +37,8 @@ import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @@ -72,8 +74,12 @@ public class BroadcastHandler implements PeerManager.Listener { private final ResultHandler resultHandler; private final String uid; - private boolean stopped, timeoutTriggered; - private int numOfCompletedBroadcasts, numOfFailedBroadcasts, numPeersForBroadcast; + private final AtomicBoolean stopped = new AtomicBoolean(); + private final AtomicBoolean timeoutTriggered = new AtomicBoolean(); + private final AtomicInteger numOfCompletedBroadcasts = new AtomicInteger(); + private final AtomicInteger numOfFailedBroadcasts = new AtomicInteger(); + private final AtomicInteger numPeersForBroadcast = new AtomicInteger(); + private Timer timeoutTimer; @@ -105,29 +111,29 @@ public class BroadcastHandler implements PeerManager.Listener { if (shutDownRequested) { delay = 1; // We sent to all peers as in case we had offers we want that it gets removed with higher reliability - numPeersForBroadcast = confirmedConnections.size(); + numPeersForBroadcast.set(confirmedConnections.size()); } else { if (requestsContainOwnMessage(broadcastRequests)) { // The broadcastRequests contains at least 1 message we have originated, so we send to all peers and // with shorter delay - numPeersForBroadcast = confirmedConnections.size(); + numPeersForBroadcast.set(confirmedConnections.size()); delay = 50; } else { // Relay nodes only send to max 7 peers and with longer delay - numPeersForBroadcast = Math.min(7, confirmedConnections.size()); + numPeersForBroadcast.set(Math.min(7, confirmedConnections.size())); delay = 100; } } setupTimeoutHandler(broadcastRequests, delay, shutDownRequested); - int iterations = numPeersForBroadcast; + int iterations = numPeersForBroadcast.get(); for (int i = 0; i < iterations; i++) { long minDelay = (i + 1) * delay; long maxDelay = (i + 2) * delay; Connection connection = confirmedConnections.get(i); UserThread.runAfterRandomDelay(() -> { - if (stopped) { + if (stopped.get()) { return; } @@ -139,8 +145,8 @@ public class BroadcastHandler implements PeerManager.Listener { // Could be empty list... if (broadcastRequestsForConnection.isEmpty()) { // We decrease numPeers in that case for making completion checks correct. - if (numPeersForBroadcast > 0) { - numPeersForBroadcast--; + if (numPeersForBroadcast.get() > 0) { + numPeersForBroadcast.decrementAndGet(); } checkForCompletion(); return; @@ -149,8 +155,8 @@ public class BroadcastHandler implements PeerManager.Listener { if (connection.isStopped()) { // Connection has died in the meantime. We skip it. // We decrease numPeers in that case for making completion checks correct. - if (numPeersForBroadcast > 0) { - numPeersForBroadcast--; + if (numPeersForBroadcast.get() > 0) { + numPeersForBroadcast.decrementAndGet(); } checkForCompletion(); return; @@ -162,7 +168,7 @@ public class BroadcastHandler implements PeerManager.Listener { } public void cancel() { - stopped = true; + stopped.set(true); cleanup(); } @@ -203,13 +209,13 @@ public class BroadcastHandler implements PeerManager.Listener { boolean shutDownRequested) { // In case of shutdown we try to complete fast and set a short 1 second timeout long baseTimeoutMs = shutDownRequested ? TimeUnit.SECONDS.toMillis(1) : BASE_TIMEOUT_MS; - long timeoutDelay = baseTimeoutMs + delay * (numPeersForBroadcast + 1); // We added 1 in the loop + long timeoutDelay = baseTimeoutMs + delay * (numPeersForBroadcast.get() + 1); // We added 1 in the loop timeoutTimer = UserThread.runAfter(() -> { - if (stopped) { + if (stopped.get()) { return; } - timeoutTriggered = true; + timeoutTriggered.set(true); log.warn("Broadcast did not complete after {} sec.\n" + "numPeersForBroadcast={}\n" + @@ -248,9 +254,9 @@ public class BroadcastHandler implements PeerManager.Listener { Futures.addCallback(future, new FutureCallback<>() { @Override public void onSuccess(Connection connection) { - numOfCompletedBroadcasts++; + numOfCompletedBroadcasts.incrementAndGet(); - if (stopped) { + if (stopped.get()) { return; } @@ -262,9 +268,9 @@ public class BroadcastHandler implements PeerManager.Listener { public void onFailure(@NotNull Throwable throwable) { log.warn("Broadcast to {} failed. ErrorMessage={}", connection.getPeersNodeAddressOptional(), throwable.getMessage()); - numOfFailedBroadcasts++; + numOfFailedBroadcasts.incrementAndGet(); - if (stopped) { + if (stopped.get()) { return; } @@ -286,9 +292,9 @@ public class BroadcastHandler implements PeerManager.Listener { } private void maybeNotifyListeners(List broadcastRequests) { - int numOfCompletedBroadcastsTarget = Math.max(1, Math.min(numPeersForBroadcast, 3)); + int numOfCompletedBroadcastsTarget = Math.max(1, Math.min(numPeersForBroadcast.get(), 3)); // We use equal checks to avoid duplicated listener calls as it would be the case with >= checks. - if (numOfCompletedBroadcasts == numOfCompletedBroadcastsTarget) { + if (numOfCompletedBroadcasts.get() == numOfCompletedBroadcastsTarget) { // We have heard back from 3 peers (or all peers if numPeers is lower) so we consider the message was sufficiently broadcast. broadcastRequests.stream() .filter(broadcastRequest -> broadcastRequest.getListener() != null) @@ -297,28 +303,28 @@ public class BroadcastHandler implements PeerManager.Listener { } else { // We check if number of open requests to peers is less than we need to reach numOfCompletedBroadcastsTarget. // Thus we never can reach required resilience as too many numOfFailedBroadcasts occurred. - int maxPossibleSuccessCases = numPeersForBroadcast - numOfFailedBroadcasts; + int maxPossibleSuccessCases = numPeersForBroadcast.get() - numOfFailedBroadcasts.get(); // We subtract 1 as we want to have it called only once, with a < comparision we would trigger repeatedly. boolean notEnoughSucceededOrOpen = maxPossibleSuccessCases == numOfCompletedBroadcastsTarget - 1; // We did not reach resilience level and timeout prevents to reach it later - boolean timeoutAndNotEnoughSucceeded = timeoutTriggered && numOfCompletedBroadcasts < numOfCompletedBroadcastsTarget; + boolean timeoutAndNotEnoughSucceeded = timeoutTriggered.get() && numOfCompletedBroadcasts.get() < numOfCompletedBroadcastsTarget; if (notEnoughSucceededOrOpen || timeoutAndNotEnoughSucceeded) { broadcastRequests.stream() .filter(broadcastRequest -> broadcastRequest.getListener() != null) .map(Broadcaster.BroadcastRequest::getListener) - .forEach(listener -> listener.onNotSufficientlyBroadcast(numOfCompletedBroadcasts, numOfFailedBroadcasts)); + .forEach(listener -> listener.onNotSufficientlyBroadcast(numOfCompletedBroadcasts.get(), numOfFailedBroadcasts.get())); } } } private void checkForCompletion() { - if (numOfCompletedBroadcasts + numOfFailedBroadcasts == numPeersForBroadcast) { + if (numOfCompletedBroadcasts.get() + numOfFailedBroadcasts.get() == numPeersForBroadcast.get()) { cleanup(); } } private void cleanup() { - stopped = true; + stopped.set(true); if (timeoutTimer != null) { timeoutTimer.stop(); timeoutTimer = null; From 20b39a4055ad1ad75d219760e97010933fcf309b Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 4 Jan 2023 13:06:24 -0500 Subject: [PATCH 60/89] Do not send close message to banned node Signed-off-by: HenrikJannsen --- .../java/bisq/network/p2p/network/CloseConnectionReason.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/src/main/java/bisq/network/p2p/network/CloseConnectionReason.java b/p2p/src/main/java/bisq/network/p2p/network/CloseConnectionReason.java index 2f715d55c5..02bac9d100 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/CloseConnectionReason.java +++ b/p2p/src/main/java/bisq/network/p2p/network/CloseConnectionReason.java @@ -44,7 +44,7 @@ public enum CloseConnectionReason { // illegal requests RULE_VIOLATION(true, false), - PEER_BANNED(true, false), + PEER_BANNED(false, false), INVALID_CLASS_RECEIVED(false, false), MANDATORY_CAPABILITIES_NOT_SUPPORTED(false, false); From de323399268dbcd99a0de00a37b7dfefc4ae06cc Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 4 Jan 2023 13:08:53 -0500 Subject: [PATCH 61/89] Remove stop setter at cancel Signed-off-by: HenrikJannsen --- p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java | 1 - 1 file changed, 1 deletion(-) diff --git a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java index 7f33f233e0..ddcb9fa1e8 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java @@ -168,7 +168,6 @@ public class BroadcastHandler implements PeerManager.Listener { } public void cancel() { - stopped.set(true); cleanup(); } From 3e48956227a0f893f891875370bc6f347ad63d23 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 4 Jan 2023 13:11:46 -0500 Subject: [PATCH 62/89] Increase numOfFailedBroadcasts at timeout Signed-off-by: HenrikJannsen --- p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java | 1 + 1 file changed, 1 insertion(+) diff --git a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java index ddcb9fa1e8..56ae58b958 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java @@ -215,6 +215,7 @@ public class BroadcastHandler implements PeerManager.Listener { } timeoutTriggered.set(true); + numOfFailedBroadcasts.incrementAndGet(); log.warn("Broadcast did not complete after {} sec.\n" + "numPeersForBroadcast={}\n" + From 5e29bfe4c21b78c783a1915754860a112edf7d98 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 4 Jan 2023 13:55:20 -0500 Subject: [PATCH 63/89] Maintain pending futures and cancel them at cleanup. Signed-off-by: HenrikJannsen --- .../network/p2p/peers/BroadcastHandler.java | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java index 56ae58b958..88b25d90bd 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java @@ -35,7 +35,10 @@ import com.google.common.util.concurrent.SettableFuture; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.UUID; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -44,6 +47,7 @@ import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; @Slf4j public class BroadcastHandler implements PeerManager.Listener { @@ -79,8 +83,9 @@ public class BroadcastHandler implements PeerManager.Listener { private final AtomicInteger numOfCompletedBroadcasts = new AtomicInteger(); private final AtomicInteger numOfFailedBroadcasts = new AtomicInteger(); private final AtomicInteger numPeersForBroadcast = new AtomicInteger(); - + @Nullable private Timer timeoutTimer; + private final Set> sendMessageFutures = new CopyOnWriteArraySet<>(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -104,6 +109,10 @@ public class BroadcastHandler implements PeerManager.Listener { public void broadcast(List broadcastRequests, boolean shutDownRequested, ListeningExecutorService executor) { + if (broadcastRequests.isEmpty()) { + return; + } + List confirmedConnections = new ArrayList<>(networkNode.getConfirmedConnections()); Collections.shuffle(confirmedConnections); @@ -162,7 +171,12 @@ public class BroadcastHandler implements PeerManager.Listener { return; } - sendToPeer(connection, broadcastRequestsForConnection, executor); + try { + sendToPeer(connection, broadcastRequestsForConnection, executor); + } catch (RejectedExecutionException e) { + log.error("RejectedExecutionException at broadcast ", e); + cleanup(); + } }, minDelay, maxDelay, TimeUnit.MILLISECONDS); } } @@ -250,7 +264,7 @@ public class BroadcastHandler implements PeerManager.Listener { // Can be BundleOfEnvelopes or a single BroadcastMessage BroadcastMessage broadcastMessage = getMessage(broadcastRequestsForConnection); SettableFuture future = networkNode.sendMessage(connection, broadcastMessage, executor); - + sendMessageFutures.add(future); Futures.addCallback(future, new FutureCallback<>() { @Override public void onSuccess(Connection connection) { @@ -324,11 +338,22 @@ public class BroadcastHandler implements PeerManager.Listener { } private void cleanup() { + if (stopped.get()) { + return; + } + stopped.set(true); + if (timeoutTimer != null) { timeoutTimer.stop(); timeoutTimer = null; } + + sendMessageFutures.stream() + .filter(future -> !future.isCancelled() && !future.isDone()) + .forEach(future -> future.cancel(true)); + sendMessageFutures.clear(); + peerManager.removeListener(this); resultHandler.onCompleted(this); } From 5efd13a678b0da8a64b1a421edb0845752463e89 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 4 Jan 2023 18:09:04 -0500 Subject: [PATCH 64/89] Check if setException returns false and if so, cancel future. Signed-off-by: HenrikJannsen --- .../bisq/core/provider/fee/FeeRequest.java | 5 ++++- .../core/provider/mempool/MempoolRequest.java | 5 ++++- .../core/provider/price/PriceRequest.java | 5 ++++- .../bisq/network/p2p/network/NetworkNode.java | 21 ++++++++++++++++--- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/bisq/core/provider/fee/FeeRequest.java b/core/src/main/java/bisq/core/provider/fee/FeeRequest.java index 007a5d3959..da3a0d9cc2 100644 --- a/core/src/main/java/bisq/core/provider/fee/FeeRequest.java +++ b/core/src/main/java/bisq/core/provider/fee/FeeRequest.java @@ -56,7 +56,10 @@ public class FeeRequest { } public void onFailure(@NotNull Throwable throwable) { - resultFuture.setException(throwable); + if (!resultFuture.setException(throwable)) { + // In case the setException returns false we need to cancel the future. + resultFuture.cancel(true); + } } }, MoreExecutors.directExecutor()); diff --git a/core/src/main/java/bisq/core/provider/mempool/MempoolRequest.java b/core/src/main/java/bisq/core/provider/mempool/MempoolRequest.java index 6141d2ed22..a70ee735ce 100644 --- a/core/src/main/java/bisq/core/provider/mempool/MempoolRequest.java +++ b/core/src/main/java/bisq/core/provider/mempool/MempoolRequest.java @@ -69,7 +69,10 @@ public class MempoolRequest { } public void onFailure(@NotNull Throwable throwable) { - mempoolServiceCallback.setException(throwable); + if (!mempoolServiceCallback.setException(throwable)) { + // In case the setException returns false we need to cancel the future. + mempoolServiceCallback.cancel(true); + } } }, MoreExecutors.directExecutor()); } diff --git a/core/src/main/java/bisq/core/provider/price/PriceRequest.java b/core/src/main/java/bisq/core/provider/price/PriceRequest.java index fa9134099c..39f4d1a985 100644 --- a/core/src/main/java/bisq/core/provider/price/PriceRequest.java +++ b/core/src/main/java/bisq/core/provider/price/PriceRequest.java @@ -65,7 +65,10 @@ public class PriceRequest { public void onFailure(@NotNull Throwable throwable) { if (!shutDownRequested) { - resultFuture.setException(new PriceRequestException(throwable, baseUrl)); + if (!resultFuture.setException(new PriceRequestException(throwable, baseUrl))) { + // In case the setException returns false we need to cancel the future. + resultFuture.cancel(true); + } } } }, MoreExecutors.directExecutor()); diff --git a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java index f4f6a5a321..4401347f67 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java +++ b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java @@ -240,7 +240,12 @@ public abstract class NetworkNode implements MessageListener { public void onFailure(@NotNull Throwable throwable) { log.debug("onFailure at sendMessage: peersNodeAddress={}\n\tmessage={}\n\tthrowable={}", peersNodeAddress, networkEnvelope.getClass().getSimpleName(), throwable.toString()); - UserThread.execute(() -> resultFuture.setException(throwable)); + UserThread.execute(() -> { + if (!resultFuture.setException(throwable)) { + // In case the setException returns false we need to cancel the future. + resultFuture.cancel(true); + } + }); } }, MoreExecutors.directExecutor()); @@ -311,13 +316,23 @@ public abstract class NetworkNode implements MessageListener { } public void onFailure(@NotNull Throwable throwable) { - UserThread.execute(() -> resultFuture.setException(throwable)); + UserThread.execute(() -> { + if (!resultFuture.setException(throwable)) { + // In case the setException returns false we need to cancel the future. + resultFuture.cancel(true); + } + }); } }, MoreExecutors.directExecutor()); } catch (RejectedExecutionException exception) { log.error("RejectedExecutionException at sendMessage: ", exception); - resultFuture.setException(exception); + UserThread.execute(() -> { + if (!resultFuture.setException(exception)) { + // In case the setException returns false we need to cancel the future. + resultFuture.cancel(true); + } + }); } return resultFuture; } From 1172eec2ced5e5b828290f950c2e9e7b4662fcc3 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 4 Jan 2023 18:14:06 -0500 Subject: [PATCH 65/89] Improve log Signed-off-by: HenrikJannsen --- p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java index 88b25d90bd..e99fbadab3 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java @@ -280,8 +280,7 @@ public class BroadcastHandler implements PeerManager.Listener { @Override public void onFailure(@NotNull Throwable throwable) { - log.warn("Broadcast to {} failed. ErrorMessage={}", connection.getPeersNodeAddressOptional(), - throwable.getMessage()); + log.warn("Broadcast to " + connection.getPeersNodeAddressOptional() + " failed. ", throwable); numOfFailedBroadcasts.incrementAndGet(); if (stopped.get()) { From 819c6fb200675997c733478e423364431b5c6821 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 4 Jan 2023 19:30:02 -0500 Subject: [PATCH 66/89] Combine nested if statements Signed-off-by: HenrikJannsen --- .../main/java/bisq/core/provider/price/PriceRequest.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/bisq/core/provider/price/PriceRequest.java b/core/src/main/java/bisq/core/provider/price/PriceRequest.java index 39f4d1a985..90861c035e 100644 --- a/core/src/main/java/bisq/core/provider/price/PriceRequest.java +++ b/core/src/main/java/bisq/core/provider/price/PriceRequest.java @@ -60,15 +60,12 @@ public class PriceRequest { if (!shutDownRequested) { resultFuture.set(marketPriceTuple); } - } public void onFailure(@NotNull Throwable throwable) { - if (!shutDownRequested) { - if (!resultFuture.setException(new PriceRequestException(throwable, baseUrl))) { - // In case the setException returns false we need to cancel the future. - resultFuture.cancel(true); - } + if (!shutDownRequested && !resultFuture.setException(new PriceRequestException(throwable, baseUrl))) { + // In case the setException returns false we need to cancel the future. + resultFuture.cancel(true); } } }, MoreExecutors.directExecutor()); From ea285e6ad10e146cb77320d1ef3355c160e0aa01 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 4 Jan 2023 20:52:22 -0500 Subject: [PATCH 67/89] Run processAccountingBlocks async in forkjoinpool thread Signed-off-by: HenrikJannsen --- .../accounting/node/AccountingNode.java | 6 +- .../node/lite/AccountingLiteNode.java | 63 +++++++++++-------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/accounting/node/AccountingNode.java b/core/src/main/java/bisq/core/dao/burningman/accounting/node/AccountingNode.java index 4572938e45..411fe17afe 100644 --- a/core/src/main/java/bisq/core/dao/burningman/accounting/node/AccountingNode.java +++ b/core/src/main/java/bisq/core/dao/burningman/accounting/node/AccountingNode.java @@ -64,11 +64,15 @@ public abstract class AccountingNode implements DaoSetupService, DaoStateListene @Nullable public static Sha256Hash getSha256Hash(Collection blocks) { + long ts = System.currentTimeMillis(); try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { for (AccountingBlock accountingBlock : blocks) { outputStream.write(accountingBlock.toProtoMessage().toByteArray()); } - return Sha256Hash.of(outputStream.toByteArray()); + Sha256Hash hash = Sha256Hash.of(outputStream.toByteArray()); + // 2833 blocks takes about 23 ms + log.info("getSha256Hash for {} blocks took {} ms", blocks.size(), System.currentTimeMillis() - ts); + return hash; } catch (IOException e) { return null; } diff --git a/core/src/main/java/bisq/core/dao/burningman/accounting/node/lite/AccountingLiteNode.java b/core/src/main/java/bisq/core/dao/burningman/accounting/node/lite/AccountingLiteNode.java index d6c437682c..19ba34481d 100644 --- a/core/src/main/java/bisq/core/dao/burningman/accounting/node/lite/AccountingLiteNode.java +++ b/core/src/main/java/bisq/core/dao/burningman/accounting/node/lite/AccountingLiteNode.java @@ -48,6 +48,8 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; import lombok.extern.slf4j.Slf4j; @@ -191,35 +193,44 @@ public class AccountingLiteNode extends AccountingNode implements AccountingLite /////////////////////////////////////////////////////////////////////////////////////////// private void processAccountingBlocks(List blocks) { - log.info("We received blocks from height {} to {}", - blocks.get(0).getHeight(), - blocks.get(blocks.size() - 1).getHeight()); + CompletableFuture.runAsync(() -> { + long ts = System.currentTimeMillis(); + log.info("We received blocks from height {} to {}", + blocks.get(0).getHeight(), + blocks.get(blocks.size() - 1).getHeight()); - boolean requiresReOrg = false; - for (AccountingBlock block : blocks) { - try { - burningManAccountingService.addBlock(block); - } catch (BlockHeightNotConnectingException e) { - log.info("Height not connecting. This could happen if we received multiple responses and had already applied a previous one. {}", e.toString()); - } catch (BlockHashNotConnectingException e) { - log.warn("Interrupt loop because a reorg is required. {}", e.toString()); - requiresReOrg = true; - break; + AtomicBoolean requiresReOrg = new AtomicBoolean(false); + for (AccountingBlock block : blocks) { + try { + burningManAccountingService.addBlock(block); + } catch (BlockHeightNotConnectingException e) { + log.info("Height not connecting. This could happen if we received multiple responses and had already applied a previous one. {}", e.toString()); + } catch (BlockHashNotConnectingException e) { + log.warn("Interrupt loop because a reorg is required. {}", e.toString()); + requiresReOrg.set(true); + break; + } } - } - if (requiresReOrg) { - applyReOrg(); - return; - } - int heightOfLastBlock = burningManAccountingService.getBlockHeightOfLastBlock(); - if (walletsSetup.isDownloadComplete() && heightOfLastBlock < bsqWalletService.getBestChainHeight()) { - accountingLiteNodeNetworkService.requestBlocks(heightOfLastBlock + 1); - } else { - if (!initialBlockRequestsComplete) { - onInitialBlockRequestsComplete(); - } - } + UserThread.execute(() -> { + if (requiresReOrg.get()) { + applyReOrg(); + return; + } + + int heightOfLastBlock = burningManAccountingService.getBlockHeightOfLastBlock(); + if (walletsSetup.isDownloadComplete() && heightOfLastBlock < bsqWalletService.getBestChainHeight()) { + accountingLiteNodeNetworkService.requestBlocks(heightOfLastBlock + 1); + } else { + if (!initialBlockRequestsComplete) { + onInitialBlockRequestsComplete(); + } + } + + // 2833 blocks takes about 24 sec + log.info("processAccountingBlocksAsync for {} blocks took {} ms", blocks.size(), System.currentTimeMillis() - ts); + }); + }); } private void processNewAccountingBlock(AccountingBlock accountingBlock) { From 02093b986b3c8a113034ab8f65ffb44c8836c22b Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 5 Jan 2023 10:06:49 -0500 Subject: [PATCH 68/89] Improve log Signed-off-by: HenrikJannsen --- core/src/main/java/bisq/core/btc/wallet/WalletService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/btc/wallet/WalletService.java b/core/src/main/java/bisq/core/btc/wallet/WalletService.java index 8cd19d7dcb..f2c3d251c6 100644 --- a/core/src/main/java/bisq/core/btc/wallet/WalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/WalletService.java @@ -291,7 +291,8 @@ public abstract class WalletService { continue; } if (!connectedOutput.isMine(wallet)) { - log.error("connectedOutput is not mine"); + log.info("ConnectedOutput is not mine. This can be the case for BSQ transactions where the " + + "input gets signed by the other wallet. connectedOutput={}", connectedOutput); continue; } From f4d335b62462c02c28a937817a6c3d030328ea48 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 5 Jan 2023 10:18:42 -0500 Subject: [PATCH 69/89] Improve logs Signed-off-by: HenrikJannsen --- .../tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java | 10 +++++++--- .../buyer/BuyerVerifiesPreparedDelayedPayoutTx.java | 9 ++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java index ba05dacec9..969edf5c5b 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java @@ -32,7 +32,6 @@ import java.util.List; import lombok.extern.slf4j.Slf4j; -import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j @@ -74,8 +73,13 @@ public class BuyerVerifiesFinalDelayedPayoutTx extends TradeTask { depositTx, delayedPayoutTxReceivers, lockTime); - checkArgument(buyersDelayedPayoutTx.getTxId().equals(finalDelayedPayoutTx.getTxId()), - "TxIds of buyersDelayedPayoutTx and finalDelayedPayoutTx must be the same"); + + if (!buyersDelayedPayoutTx.getTxId().equals(finalDelayedPayoutTx.getTxId())) { + String errorMsg = "TxIds of buyersDelayedPayoutTx and finalDelayedPayoutTx must be the same."; + log.error("{} \nbuyersDelayedPayoutTx={}, \nfinalDelayedPayoutTx={}", + errorMsg, buyersDelayedPayoutTx, finalDelayedPayoutTx); + throw new IllegalArgumentException(errorMsg); + } } complete(); diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java index 9f7e9c0d5f..a1563e0642 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java @@ -32,7 +32,6 @@ import java.util.List; import lombok.extern.slf4j.Slf4j; -import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j @@ -66,8 +65,12 @@ public class BuyerVerifiesPreparedDelayedPayoutTx extends TradeTask { preparedDepositTx, delayedPayoutTxReceivers, lockTime); - checkArgument(buyersPreparedDelayedPayoutTx.getTxId().equals(sellersPreparedDelayedPayoutTx.getTxId()), - "TxIds of buyersPreparedDelayedPayoutTx and sellersPreparedDelayedPayoutTx must be the same"); + if (!buyersPreparedDelayedPayoutTx.getTxId().equals(sellersPreparedDelayedPayoutTx.getTxId())) { + String errorMsg = "TxIds of buyersPreparedDelayedPayoutTx and sellersPreparedDelayedPayoutTx must be the same."; + log.error("{} \nbuyersPreparedDelayedPayoutTx={}, \nsellersPreparedDelayedPayoutTx={}", + errorMsg, buyersPreparedDelayedPayoutTx, sellersPreparedDelayedPayoutTx); + throw new IllegalArgumentException(errorMsg); + } } // If the deposit tx is non-malleable, we already know its final ID, so should check that now From be33aa5236662c88abe62cdb9eb0ad68681af2b5 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 5 Jan 2023 10:22:50 -0500 Subject: [PATCH 70/89] Add chain heights to logs Signed-off-by: HenrikJannsen --- .../tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java | 8 ++++++-- .../tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java | 7 +++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java index 969edf5c5b..56f8ba7c44 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java @@ -76,8 +76,12 @@ public class BuyerVerifiesFinalDelayedPayoutTx extends TradeTask { if (!buyersDelayedPayoutTx.getTxId().equals(finalDelayedPayoutTx.getTxId())) { String errorMsg = "TxIds of buyersDelayedPayoutTx and finalDelayedPayoutTx must be the same."; - log.error("{} \nbuyersDelayedPayoutTx={}, \nfinalDelayedPayoutTx={}", - errorMsg, buyersDelayedPayoutTx, finalDelayedPayoutTx); + log.error("{} \nbuyersDelayedPayoutTx={}, \nfinalDelayedPayoutTx={}, " + + "\nBtcWalletService.chainHeight={}, \nDaoState.chainHeight={}", + errorMsg, buyersDelayedPayoutTx, finalDelayedPayoutTx, + processModel.getBtcWalletService().getBestChainHeight(), + processModel.getDaoFacade().getChainHeight()); + throw new IllegalArgumentException(errorMsg); } } diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java index a1563e0642..d55690b640 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java @@ -67,8 +67,11 @@ public class BuyerVerifiesPreparedDelayedPayoutTx extends TradeTask { lockTime); if (!buyersPreparedDelayedPayoutTx.getTxId().equals(sellersPreparedDelayedPayoutTx.getTxId())) { String errorMsg = "TxIds of buyersPreparedDelayedPayoutTx and sellersPreparedDelayedPayoutTx must be the same."; - log.error("{} \nbuyersPreparedDelayedPayoutTx={}, \nsellersPreparedDelayedPayoutTx={}", - errorMsg, buyersPreparedDelayedPayoutTx, sellersPreparedDelayedPayoutTx); + log.error("{} \nbuyersPreparedDelayedPayoutTx={}, \nsellersPreparedDelayedPayoutTx={}, " + + "\nBtcWalletService.chainHeight={}, \nDaoState.chainHeight={}", + errorMsg, buyersPreparedDelayedPayoutTx, sellersPreparedDelayedPayoutTx, + processModel.getBtcWalletService().getBestChainHeight(), + processModel.getDaoFacade().getChainHeight()); throw new IllegalArgumentException(errorMsg); } } From b33c61001541d3cf3a0fb4eed7796a38dafc3a95 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 5 Jan 2023 10:28:59 -0500 Subject: [PATCH 71/89] Add CheckIfDaoStateIsInSync task in trade protocol as first task Add ifDaoStateIsInSync method to DaoFacade Add ifDaoStateIsInSync to logs if DPT verification fails --- .../main/java/bisq/core/dao/DaoFacade.java | 5 +++ .../bisq_v1/BuyerAsMakerProtocol.java | 2 + .../bisq_v1/BuyerAsTakerProtocol.java | 2 + .../bisq_v1/SellerAsMakerProtocol.java | 2 + .../bisq_v1/SellerAsTakerProtocol.java | 2 + .../tasks/CheckIfDaoStateIsInSync.java | 45 +++++++++++++++++++ .../BuyerVerifiesFinalDelayedPayoutTx.java | 8 +++- .../BuyerVerifiesPreparedDelayedPayoutTx.java | 7 ++- 8 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/CheckIfDaoStateIsInSync.java diff --git a/core/src/main/java/bisq/core/dao/DaoFacade.java b/core/src/main/java/bisq/core/dao/DaoFacade.java index b4729179d2..51f6b94aa1 100644 --- a/core/src/main/java/bisq/core/dao/DaoFacade.java +++ b/core/src/main/java/bisq/core/dao/DaoFacade.java @@ -813,4 +813,9 @@ public class DaoFacade implements DaoSetupService { public boolean isParseBlockChainComplete() { return daoStateService.isParseBlockChainComplete(); } + + public boolean isDaoStateIsInSync() { + return !daoStateMonitoringService.isInConflictWithSeedNode() && + !daoStateMonitoringService.isDaoStateBlockChainNotConnecting(); + } } diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/BuyerAsMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/BuyerAsMakerProtocol.java index 8f161351aa..83bda8ac8b 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/BuyerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/BuyerAsMakerProtocol.java @@ -25,6 +25,7 @@ import bisq.core.trade.protocol.bisq_v1.messages.DepositTxAndDelayedPayoutTxMess import bisq.core.trade.protocol.bisq_v1.messages.InputsForDepositTxRequest; import bisq.core.trade.protocol.bisq_v1.messages.PayoutTxPublishedMessage; import bisq.core.trade.protocol.bisq_v1.tasks.ApplyFilter; +import bisq.core.trade.protocol.bisq_v1.tasks.CheckIfDaoStateIsInSync; import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask; import bisq.core.trade.protocol.bisq_v1.tasks.buyer.BuyerFinalizesDelayedPayoutTx; import bisq.core.trade.protocol.bisq_v1.tasks.buyer.BuyerProcessDelayedPayoutTxSignatureRequest; @@ -71,6 +72,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol .with(message) .from(peer)) .setup(tasks( + CheckIfDaoStateIsInSync.class, MakerProcessesInputsForDepositTxRequest.class, ApplyFilter.class, getVerifyPeersFeePaymentClass(), diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/BuyerAsTakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/BuyerAsTakerProtocol.java index 8a1d927eef..d7b9413db5 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/BuyerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/BuyerAsTakerProtocol.java @@ -27,6 +27,7 @@ import bisq.core.trade.protocol.bisq_v1.messages.DepositTxAndDelayedPayoutTxMess import bisq.core.trade.protocol.bisq_v1.messages.InputsForDepositTxResponse; import bisq.core.trade.protocol.bisq_v1.messages.PayoutTxPublishedMessage; import bisq.core.trade.protocol.bisq_v1.tasks.ApplyFilter; +import bisq.core.trade.protocol.bisq_v1.tasks.CheckIfDaoStateIsInSync; import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask; import bisq.core.trade.protocol.bisq_v1.tasks.buyer.BuyerFinalizesDelayedPayoutTx; import bisq.core.trade.protocol.bisq_v1.tasks.buyer.BuyerProcessDelayedPayoutTxSignatureRequest; @@ -77,6 +78,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol expect(phase(Trade.Phase.INIT) .with(TakerEvent.TAKE_OFFER)) .setup(tasks( + CheckIfDaoStateIsInSync.class, ApplyFilter.class, getVerifyPeersFeePaymentClass(), CreateTakerFeeTx.class, diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/SellerAsMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/SellerAsMakerProtocol.java index 1da30e3e0d..c88e7de0f8 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/SellerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/SellerAsMakerProtocol.java @@ -27,6 +27,7 @@ import bisq.core.trade.protocol.bisq_v1.messages.DelayedPayoutTxSignatureRespons import bisq.core.trade.protocol.bisq_v1.messages.DepositTxMessage; import bisq.core.trade.protocol.bisq_v1.messages.InputsForDepositTxRequest; import bisq.core.trade.protocol.bisq_v1.tasks.ApplyFilter; +import bisq.core.trade.protocol.bisq_v1.tasks.CheckIfDaoStateIsInSync; import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask; import bisq.core.trade.protocol.bisq_v1.tasks.maker.MakerCreateAndSignContract; import bisq.core.trade.protocol.bisq_v1.tasks.maker.MakerProcessesInputsForDepositTxRequest; @@ -73,6 +74,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc .with(message) .from(peer)) .setup(tasks( + CheckIfDaoStateIsInSync.class, MaybeCreateSubAccount.class, MakerProcessesInputsForDepositTxRequest.class, ApplyFilter.class, diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/SellerAsTakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/SellerAsTakerProtocol.java index fae836be76..42804e7cd3 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/SellerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/SellerAsTakerProtocol.java @@ -26,6 +26,7 @@ import bisq.core.trade.protocol.bisq_v1.messages.CounterCurrencyTransferStartedM import bisq.core.trade.protocol.bisq_v1.messages.DelayedPayoutTxSignatureResponse; import bisq.core.trade.protocol.bisq_v1.messages.InputsForDepositTxResponse; import bisq.core.trade.protocol.bisq_v1.tasks.ApplyFilter; +import bisq.core.trade.protocol.bisq_v1.tasks.CheckIfDaoStateIsInSync; import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask; import bisq.core.trade.protocol.bisq_v1.tasks.seller.MaybeCreateSubAccount; import bisq.core.trade.protocol.bisq_v1.tasks.seller.SellerCreatesDelayedPayoutTx; @@ -73,6 +74,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc .with(TakerEvent.TAKE_OFFER) .from(trade.getTradingPeerNodeAddress())) .setup(tasks( + CheckIfDaoStateIsInSync.class, MaybeCreateSubAccount.class, ApplyFilter.class, getVerifyPeersFeePaymentClass(), diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/CheckIfDaoStateIsInSync.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/CheckIfDaoStateIsInSync.java new file mode 100644 index 0000000000..e70e7e9cae --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/CheckIfDaoStateIsInSync.java @@ -0,0 +1,45 @@ +/* + * 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.trade.protocol.bisq_v1.tasks; + +import bisq.core.trade.model.bisq_v1.Trade; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; + +@Slf4j +public class CheckIfDaoStateIsInSync extends TradeTask { + public CheckIfDaoStateIsInSync(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + checkArgument(processModel.getDaoFacade().isDaoStateIsInSync(), "DAO state is not in sync with seed nodes"); + } catch (Throwable t) { + failed(t); + } + } +} + diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java index 56f8ba7c44..66e8361d88 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java @@ -75,12 +75,16 @@ public class BuyerVerifiesFinalDelayedPayoutTx extends TradeTask { lockTime); if (!buyersDelayedPayoutTx.getTxId().equals(finalDelayedPayoutTx.getTxId())) { + String errorMsg = "TxIds of buyersDelayedPayoutTx and finalDelayedPayoutTx must be the same."; log.error("{} \nbuyersDelayedPayoutTx={}, \nfinalDelayedPayoutTx={}, " + - "\nBtcWalletService.chainHeight={}, \nDaoState.chainHeight={}", + "\nBtcWalletService.chainHeight={}, " + + "\nDaoState.chainHeight={}, " + + "\nisDaoStateIsInSync={}", errorMsg, buyersDelayedPayoutTx, finalDelayedPayoutTx, processModel.getBtcWalletService().getBestChainHeight(), - processModel.getDaoFacade().getChainHeight()); + processModel.getDaoFacade().getChainHeight(), + processModel.getDaoFacade().isDaoStateIsInSync()); throw new IllegalArgumentException(errorMsg); } diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java index d55690b640..fe3e580086 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java @@ -68,10 +68,13 @@ public class BuyerVerifiesPreparedDelayedPayoutTx extends TradeTask { if (!buyersPreparedDelayedPayoutTx.getTxId().equals(sellersPreparedDelayedPayoutTx.getTxId())) { String errorMsg = "TxIds of buyersPreparedDelayedPayoutTx and sellersPreparedDelayedPayoutTx must be the same."; log.error("{} \nbuyersPreparedDelayedPayoutTx={}, \nsellersPreparedDelayedPayoutTx={}, " + - "\nBtcWalletService.chainHeight={}, \nDaoState.chainHeight={}", + "\nBtcWalletService.chainHeight={}, " + + "\nDaoState.chainHeight={}, " + + "\nisDaoStateIsInSync={}", errorMsg, buyersPreparedDelayedPayoutTx, sellersPreparedDelayedPayoutTx, processModel.getBtcWalletService().getBestChainHeight(), - processModel.getDaoFacade().getChainHeight()); + processModel.getDaoFacade().getChainHeight(), + processModel.getDaoFacade().isDaoStateIsInSync()); throw new IllegalArgumentException(errorMsg); } } From 1e2f48b9bc555080520418cbb5310be11f911f25 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 5 Jan 2023 10:42:59 -0500 Subject: [PATCH 72/89] Rename isDaoStateIsInSync to isDaoStateReadyAndInSync Add daoStateService.isParseBlockChainComplete() check Signed-off-by: HenrikJannsen --- core/src/main/java/bisq/core/dao/DaoFacade.java | 5 +++-- .../protocol/bisq_v1/tasks/CheckIfDaoStateIsInSync.java | 2 +- .../tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java | 4 +--- .../tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/DaoFacade.java b/core/src/main/java/bisq/core/dao/DaoFacade.java index 51f6b94aa1..ce445c53af 100644 --- a/core/src/main/java/bisq/core/dao/DaoFacade.java +++ b/core/src/main/java/bisq/core/dao/DaoFacade.java @@ -814,8 +814,9 @@ public class DaoFacade implements DaoSetupService { return daoStateService.isParseBlockChainComplete(); } - public boolean isDaoStateIsInSync() { - return !daoStateMonitoringService.isInConflictWithSeedNode() && + public boolean isDaoStateReadyAndInSync() { + return daoStateService.isParseBlockChainComplete() && + !daoStateMonitoringService.isInConflictWithSeedNode() && !daoStateMonitoringService.isDaoStateBlockChainNotConnecting(); } } diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/CheckIfDaoStateIsInSync.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/CheckIfDaoStateIsInSync.java index e70e7e9cae..519c5b47a8 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/CheckIfDaoStateIsInSync.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/CheckIfDaoStateIsInSync.java @@ -36,7 +36,7 @@ public class CheckIfDaoStateIsInSync extends TradeTask { try { runInterceptHook(); - checkArgument(processModel.getDaoFacade().isDaoStateIsInSync(), "DAO state is not in sync with seed nodes"); + checkArgument(processModel.getDaoFacade().isDaoStateReadyAndInSync(), "DAO state is not in sync with seed nodes"); } catch (Throwable t) { failed(t); } diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java index 66e8361d88..3bf644b1fd 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java @@ -75,7 +75,6 @@ public class BuyerVerifiesFinalDelayedPayoutTx extends TradeTask { lockTime); if (!buyersDelayedPayoutTx.getTxId().equals(finalDelayedPayoutTx.getTxId())) { - String errorMsg = "TxIds of buyersDelayedPayoutTx and finalDelayedPayoutTx must be the same."; log.error("{} \nbuyersDelayedPayoutTx={}, \nfinalDelayedPayoutTx={}, " + "\nBtcWalletService.chainHeight={}, " + @@ -84,8 +83,7 @@ public class BuyerVerifiesFinalDelayedPayoutTx extends TradeTask { errorMsg, buyersDelayedPayoutTx, finalDelayedPayoutTx, processModel.getBtcWalletService().getBestChainHeight(), processModel.getDaoFacade().getChainHeight(), - processModel.getDaoFacade().isDaoStateIsInSync()); - + processModel.getDaoFacade().isDaoStateReadyAndInSync()); throw new IllegalArgumentException(errorMsg); } } diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java index fe3e580086..6fe2321d07 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java @@ -74,7 +74,7 @@ public class BuyerVerifiesPreparedDelayedPayoutTx extends TradeTask { errorMsg, buyersPreparedDelayedPayoutTx, sellersPreparedDelayedPayoutTx, processModel.getBtcWalletService().getBestChainHeight(), processModel.getDaoFacade().getChainHeight(), - processModel.getDaoFacade().isDaoStateIsInSync()); + processModel.getDaoFacade().isDaoStateReadyAndInSync()); throw new IllegalArgumentException(errorMsg); } } From 13180ddc30835e9c72aac55791b0b457e3b1540e Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 5 Jan 2023 11:21:01 -0500 Subject: [PATCH 73/89] Improve DaoPresentation and add handler for daoStateHash updates Signed-off-by: HenrikJannsen --- .../main/java/bisq/core/dao/DaoFacade.java | 13 +-- .../java/bisq/desktop/main/MainViewModel.java | 8 +- .../main/presentation/DaoPresentation.java | 95 ++++++++++--------- .../main/java/bisq/desktop/util/GUIUtil.java | 16 ---- 4 files changed, 59 insertions(+), 73 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/DaoFacade.java b/core/src/main/java/bisq/core/dao/DaoFacade.java index ce445c53af..c014016947 100644 --- a/core/src/main/java/bisq/core/dao/DaoFacade.java +++ b/core/src/main/java/bisq/core/dao/DaoFacade.java @@ -531,10 +531,13 @@ public class DaoFacade implements DaoSetupService { return daoStateService.getBlockAtHeight(chainHeight); } - public boolean daoStateNeedsRebuilding() { - return daoStateMonitoringService.isInConflictWithSeedNode() || daoStateMonitoringService.isDaoStateBlockChainNotConnecting(); + public boolean isDaoStateReadyAndInSync() { + return daoStateService.isParseBlockChainComplete() && + !daoStateMonitoringService.isInConflictWithSeedNode() && + !daoStateMonitoringService.isDaoStateBlockChainNotConnecting(); } + /////////////////////////////////////////////////////////////////////////////////////////// // Use case: Bonding /////////////////////////////////////////////////////////////////////////////////////////// @@ -813,10 +816,4 @@ public class DaoFacade implements DaoSetupService { public boolean isParseBlockChainComplete() { return daoStateService.isParseBlockChainComplete(); } - - public boolean isDaoStateReadyAndInSync() { - return daoStateService.isParseBlockChainComplete() && - !daoStateMonitoringService.isInConflictWithSeedNode() && - !daoStateMonitoringService.isDaoStateBlockChainNotConnecting(); - } } diff --git a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java index 3eb0283a32..4c132613c3 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java @@ -310,7 +310,7 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener { setupClockWatcherPopup(); marketPricePresentation.setup(); - daoPresentation.setup(); + daoPresentation.init(); accountPresentation.setup(); settingsPresentation.setup(); @@ -505,7 +505,7 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener { .show()); bisqSetup.getBtcSyncProgress().addListener((observable, oldValue, newValue) -> updateBtcSyncProgress()); - daoPresentation.getBsqSyncProgress().addListener((observable, oldValue, newValue) -> updateBtcSyncProgress()); + daoPresentation.getDaoStateSyncProgress().addListener((observable, oldValue, newValue) -> updateBtcSyncProgress()); bisqSetup.setFilterWarningHandler(warning -> new Popup().warning(warning).show()); @@ -704,7 +704,7 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener { if (btcSyncProgress.doubleValue() < 1) { combinedSyncProgress.set(btcSyncProgress.doubleValue()); } else { - combinedSyncProgress.set(daoPresentation.getBsqSyncProgress().doubleValue()); + combinedSyncProgress.set(daoPresentation.getDaoStateSyncProgress().doubleValue()); } } @@ -783,7 +783,7 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener { StringProperty getCombinedFooterInfo() { final StringProperty combinedInfo = new SimpleStringProperty(); - combinedInfo.bind(Bindings.concat(this.footerVersionInfo, " ", daoPresentation.getBsqInfo())); + combinedInfo.bind(Bindings.concat(this.footerVersionInfo, " ", daoPresentation.getDaoStateInfo())); return combinedInfo; } diff --git a/desktop/src/main/java/bisq/desktop/main/presentation/DaoPresentation.java b/desktop/src/main/java/bisq/desktop/main/presentation/DaoPresentation.java index 0afda183c6..5c441ac363 100644 --- a/desktop/src/main/java/bisq/desktop/main/presentation/DaoPresentation.java +++ b/desktop/src/main/java/bisq/desktop/main/presentation/DaoPresentation.java @@ -1,11 +1,15 @@ package bisq.desktop.main.presentation; import bisq.desktop.Navigation; -import bisq.desktop.util.GUIUtil; +import bisq.desktop.main.MainView; +import bisq.desktop.main.dao.DaoView; +import bisq.desktop.main.dao.monitor.MonitorView; +import bisq.desktop.main.dao.monitor.daostate.DaoStateMonitorView; +import bisq.desktop.main.overlays.popups.Popup; import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; -import bisq.core.dao.DaoFacade; +import bisq.core.dao.monitoring.DaoStateMonitoringService; import bisq.core.dao.state.DaoStateListener; import bisq.core.dao.state.DaoStateService; import bisq.core.dao.state.model.blockchain.Block; @@ -28,24 +32,21 @@ import javafx.collections.MapChangeListener; import lombok.Getter; @Singleton -public class DaoPresentation implements DaoStateListener { +public class DaoPresentation implements DaoStateListener, DaoStateMonitoringService.Listener { public static final String DAO_NEWS = "daoNews"; - private final Preferences preferences; private final Navigation navigation; private final BtcWalletService btcWalletService; - private final DaoFacade daoFacade; + private final DaoStateMonitoringService daoStateMonitoringService; private final BsqWalletService bsqWalletService; private final DaoStateService daoStateService; private final ChangeListener walletChainHeightListener; - @Getter - private final DoubleProperty bsqSyncProgress = new SimpleDoubleProperty(-1); + private final DoubleProperty daoStateSyncProgress = new SimpleDoubleProperty(-1); @Getter - private final StringProperty bsqInfo = new SimpleStringProperty(""); + private final StringProperty daoStateInfo = new SimpleStringProperty(""); private final SimpleBooleanProperty showNotification = new SimpleBooleanProperty(false); - private boolean daoConflictWarningShown = false; // allow only one conflict warning per session @Inject public DaoPresentation(Preferences preferences, @@ -53,12 +54,11 @@ public class DaoPresentation implements DaoStateListener { BtcWalletService btcWalletService, BsqWalletService bsqWalletService, DaoStateService daoStateService, - DaoFacade daoFacade) { - this.preferences = preferences; + DaoStateMonitoringService daoStateMonitoringService) { this.navigation = navigation; this.btcWalletService = btcWalletService; this.bsqWalletService = bsqWalletService; - this.daoFacade = daoFacade; + this.daoStateMonitoringService = daoStateMonitoringService; this.daoStateService = daoStateService; preferences.getDontShowAgainMapAsObservable().addListener((MapChangeListener) change -> { @@ -69,38 +69,30 @@ public class DaoPresentation implements DaoStateListener { } }); + daoStateService.addDaoStateListener(this); + daoStateMonitoringService.addListener(this); + walletChainHeightListener = (observable, oldValue, newValue) -> onUpdateAnyChainHeight(); } /////////////////////////////////////////////////////////////////////////////////////////// - // Private + // Public /////////////////////////////////////////////////////////////////////////////////////////// - private void onUpdateAnyChainHeight() { - final int bsqBlockChainHeight = daoFacade.getChainHeight(); - final int bsqWalletChainHeight = bsqWalletService.getBestChainHeight(); - if (bsqWalletChainHeight > 0) { - final boolean synced = bsqWalletChainHeight == bsqBlockChainHeight; - if (bsqBlockChainHeight != bsqWalletChainHeight) { - bsqSyncProgress.set(-1); - } else { - bsqSyncProgress.set(0); - } + public void init() { + showNotification.set(false); - if (synced) { - bsqInfo.set(""); - if (daoFacade.daoStateNeedsRebuilding() && !daoConflictWarningShown) { - daoConflictWarningShown = true; // only warn max 1 time per session so as not to annoy - GUIUtil.showDaoNeedsResyncPopup(navigation); - } - } else { - bsqInfo.set(Res.get("mainView.footer.bsqInfo.synchronizing")); - } - } else { - bsqInfo.set(Res.get("mainView.footer.bsqInfo.synchronizing")); - } + btcWalletService.getChainHeightProperty().addListener(walletChainHeightListener); + daoStateService.addDaoStateListener(this); + + onUpdateAnyChainHeight(); } + public BooleanProperty getShowDaoUpdatesNotification() { + return showNotification; + } + + /////////////////////////////////////////////////////////////////////////////////////////// // DaoStateListener /////////////////////////////////////////////////////////////////////////////////////////// @@ -110,22 +102,35 @@ public class DaoPresentation implements DaoStateListener { onUpdateAnyChainHeight(); } + /////////////////////////////////////////////////////////////////////////////////////////// - // Public + // DaoStateMonitoringService.Listener /////////////////////////////////////////////////////////////////////////////////////////// - public BooleanProperty getShowDaoUpdatesNotification() { - return showNotification; + @Override + public void onDaoStateHashesChanged() { + if (daoStateService.isParseBlockChainComplete()) { + if (daoStateMonitoringService.isInConflictWithSeedNode() || + daoStateMonitoringService.isDaoStateBlockChainNotConnecting()) { + new Popup().warning(Res.get("popup.warning.daoNeedsResync")) + .actionButtonTextWithGoTo("navigation.dao.networkMonitor") + .onAction(() -> navigation.navigateTo(MainView.class, DaoView.class, MonitorView.class, DaoStateMonitorView.class)) + .show(); + } + } } - public void setup() { - // devs enable this when a news badge is required - //showNotification.set(DevEnv.isDaoActivated() && preferences.showAgain(DAO_NEWS)); - showNotification.set(false); - this.btcWalletService.getChainHeightProperty().addListener(walletChainHeightListener); - daoStateService.addDaoStateListener(this); + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// - onUpdateAnyChainHeight(); + private void onUpdateAnyChainHeight() { + int bsqWalletChainHeight = bsqWalletService.getBestChainHeight(); + int daoStateChainHeight = daoStateService.getChainHeight(); + boolean chainHeightsInSync = bsqWalletChainHeight > 0 && bsqWalletChainHeight == daoStateChainHeight; + boolean isDaoStateReady = chainHeightsInSync && daoStateService.isParseBlockChainComplete(); + daoStateSyncProgress.set(isDaoStateReady ? 0 : -1); + daoStateInfo.set(isDaoStateReady ? "" : Res.get("mainView.footer.bsqInfo.synchronizing")); } } diff --git a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java index 9c64ca9dc8..8c9406795a 100644 --- a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java @@ -27,9 +27,6 @@ import bisq.desktop.components.indicator.TxConfidenceIndicator; import bisq.desktop.main.MainView; import bisq.desktop.main.account.AccountView; import bisq.desktop.main.account.content.fiataccounts.FiatAccountsView; -import bisq.desktop.main.dao.DaoView; -import bisq.desktop.main.dao.monitor.MonitorView; -import bisq.desktop.main.dao.monitor.daostate.DaoStateMonitorView; import bisq.desktop.main.overlays.popups.Popup; import bisq.core.account.witness.AccountAgeWitness; @@ -806,19 +803,6 @@ public class GUIUtil { return false; } - public static void showDaoNeedsResyncPopup(Navigation navigation) { - String key = "showDaoNeedsResyncPopup"; - if (DontShowAgainLookup.showAgain(key)) { - UserThread.runAfter(() -> new Popup().warning(Res.get("popup.warning.daoNeedsResync")) - .dontShowAgainId(key) - .actionButtonTextWithGoTo("navigation.dao.networkMonitor") - .onAction(() -> { - navigation.navigateTo(MainView.class, DaoView.class, MonitorView.class, DaoStateMonitorView.class); - }) - .show(), 5, TimeUnit.SECONDS); - } - } - public static boolean isReadyForTxBroadcastOrShowPopup(P2PService p2PService, WalletsSetup walletsSetup) { if (!GUIUtil.isBootstrappedOrShowPopup(p2PService)) { return false; From 093e8f99f43a8a53907a4999ddfe70b3d4645d4f Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 5 Jan 2023 11:41:11 -0500 Subject: [PATCH 74/89] Remove unused methods Signed-off-by: HenrikJannsen --- core/src/main/java/bisq/core/dao/DaoFacade.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/DaoFacade.java b/core/src/main/java/bisq/core/dao/DaoFacade.java index c014016947..a3ccb9377e 100644 --- a/core/src/main/java/bisq/core/dao/DaoFacade.java +++ b/core/src/main/java/bisq/core/dao/DaoFacade.java @@ -57,7 +57,6 @@ import bisq.core.dao.monitoring.DaoStateMonitoringService; import bisq.core.dao.state.DaoStateListener; import bisq.core.dao.state.DaoStateService; import bisq.core.dao.state.model.blockchain.BaseTx; -import bisq.core.dao.state.model.blockchain.BaseTxOutput; import bisq.core.dao.state.model.blockchain.Block; import bisq.core.dao.state.model.blockchain.Tx; import bisq.core.dao.state.model.blockchain.TxOutput; @@ -581,10 +580,6 @@ public class DaoFacade implements DaoSetupService { return daoStateService.getTotalAmountOfConfiscatedTxOutputs(); } - public long getTotalAmountOfInvalidatedBsq() { - return daoStateService.getTotalAmountOfInvalidatedBsq(); - } - // Contains burned fee and invalidated bsq due invalid txs public long getTotalAmountOfBurntBsq() { return daoStateService.getTotalAmountOfBurntBsq(); @@ -598,11 +593,6 @@ public class DaoFacade implements DaoSetupService { return daoStateService.getIrregularTxs(); } - public long getTotalAmountOfUnspentTxOutputs() { - // Does not consider confiscated outputs (they stay as utxo) - return daoStateService.getUnspentTxOutputMap().values().stream().mapToLong(BaseTxOutput::getValue).sum(); - } - public Optional getLockTime(String txId) { return daoStateService.getLockTime(txId); } From cdfa3fa141db2ae1e64e37ab68656b1273905c7a Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 5 Jan 2023 12:20:09 -0500 Subject: [PATCH 75/89] Return if !isParseBlockChainComplete Signed-off-by: HenrikJannsen --- .../main/presentation/DaoPresentation.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/presentation/DaoPresentation.java b/desktop/src/main/java/bisq/desktop/main/presentation/DaoPresentation.java index 5c441ac363..344f71951d 100644 --- a/desktop/src/main/java/bisq/desktop/main/presentation/DaoPresentation.java +++ b/desktop/src/main/java/bisq/desktop/main/presentation/DaoPresentation.java @@ -109,14 +109,16 @@ public class DaoPresentation implements DaoStateListener, DaoStateMonitoringServ @Override public void onDaoStateHashesChanged() { - if (daoStateService.isParseBlockChainComplete()) { - if (daoStateMonitoringService.isInConflictWithSeedNode() || - daoStateMonitoringService.isDaoStateBlockChainNotConnecting()) { - new Popup().warning(Res.get("popup.warning.daoNeedsResync")) - .actionButtonTextWithGoTo("navigation.dao.networkMonitor") - .onAction(() -> navigation.navigateTo(MainView.class, DaoView.class, MonitorView.class, DaoStateMonitorView.class)) - .show(); - } + if (!daoStateService.isParseBlockChainComplete()) { + return; + } + + if (daoStateMonitoringService.isInConflictWithSeedNode() || + daoStateMonitoringService.isDaoStateBlockChainNotConnecting()) { + new Popup().warning(Res.get("popup.warning.daoNeedsResync")) + .actionButtonTextWithGoTo("navigation.dao.networkMonitor") + .onAction(() -> navigation.navigateTo(MainView.class, DaoView.class, MonitorView.class, DaoStateMonitorView.class)) + .show(); } } From bc91914cb0680799fd1751fbb583a565c2f95f65 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 5 Jan 2023 14:48:51 -0500 Subject: [PATCH 76/89] Change log level Signed-off-by: HenrikJannsen --- .../bisq/core/dao/node/full/network/FullNodeNetworkService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/dao/node/full/network/FullNodeNetworkService.java b/core/src/main/java/bisq/core/dao/node/full/network/FullNodeNetworkService.java index 20583433cb..430426dba5 100644 --- a/core/src/main/java/bisq/core/dao/node/full/network/FullNodeNetworkService.java +++ b/core/src/main/java/bisq/core/dao/node/full/network/FullNodeNetworkService.java @@ -206,7 +206,7 @@ public class FullNodeNetworkService implements MessageListener, PeerManager.List } private void handleRepublishGovernanceDataRequest() { - log.warn("We received a RepublishGovernanceDataRequest and re-published all proposalPayloads and " + + log.info("We received a RepublishGovernanceDataRequest and re-published all proposalPayloads and " + "blindVotePayloads to the P2P network."); missingDataRequestService.reRepublishAllGovernanceData(); } From 31592ac9553e3f8145c0b643c631c95929ec918f Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 5 Jan 2023 21:46:44 -0500 Subject: [PATCH 77/89] Update BurningManAccountingStore Signed-off-by: HenrikJannsen --- .../BurningManAccountingStore_BTC_MAINNET | Bin 4900390 -> 5063457 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/p2p/src/main/resources/BurningManAccountingStore_BTC_MAINNET b/p2p/src/main/resources/BurningManAccountingStore_BTC_MAINNET index f34d35c6c4d0051bfed83b40474f3e527d9e4430..2f7dfd651964df29b70e15748eb6fabe4b55f657 100644 GIT binary patch delta 164555 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 From aa514b37da65a27a8ddee731d859d96a474124b3 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 5 Jan 2023 22:02:57 -0500 Subject: [PATCH 78/89] Add listeners at init Remove duplicated listener registration Signed-off-by: HenrikJannsen --- .../java/bisq/desktop/main/presentation/DaoPresentation.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/presentation/DaoPresentation.java b/desktop/src/main/java/bisq/desktop/main/presentation/DaoPresentation.java index 344f71951d..a0d0da6afa 100644 --- a/desktop/src/main/java/bisq/desktop/main/presentation/DaoPresentation.java +++ b/desktop/src/main/java/bisq/desktop/main/presentation/DaoPresentation.java @@ -69,9 +69,6 @@ public class DaoPresentation implements DaoStateListener, DaoStateMonitoringServ } }); - daoStateService.addDaoStateListener(this); - daoStateMonitoringService.addListener(this); - walletChainHeightListener = (observable, oldValue, newValue) -> onUpdateAnyChainHeight(); } @@ -84,6 +81,7 @@ public class DaoPresentation implements DaoStateListener, DaoStateMonitoringServ btcWalletService.getChainHeightProperty().addListener(walletChainHeightListener); daoStateService.addDaoStateListener(this); + daoStateMonitoringService.addListener(this); onUpdateAnyChainHeight(); } From bb06f64049deb91ea0ef5a99d54dcf25f82b7812 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 5 Jan 2023 15:46:59 -0500 Subject: [PATCH 79/89] Add complete call Signed-off-by: HenrikJannsen --- .../trade/protocol/bisq_v1/tasks/CheckIfDaoStateIsInSync.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/CheckIfDaoStateIsInSync.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/CheckIfDaoStateIsInSync.java index 519c5b47a8..180f9dca9b 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/CheckIfDaoStateIsInSync.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/CheckIfDaoStateIsInSync.java @@ -37,6 +37,7 @@ public class CheckIfDaoStateIsInSync extends TradeTask { runInterceptHook(); checkArgument(processModel.getDaoFacade().isDaoStateReadyAndInSync(), "DAO state is not in sync with seed nodes"); + complete(); } catch (Throwable t) { failed(t); } From 41a63cc8a50009adf2213a267d42a908a4583749 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 5 Jan 2023 15:57:27 -0500 Subject: [PATCH 80/89] Add handling of miner fee in case there is only the legacy BM Use spendableAmount instead of inputAmount at maxOutputAmount Signed-off-by: HenrikJannsen --- .../DelayedPayoutTxReceiverService.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java index 661189daf8..1a1512938f 100644 --- a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java +++ b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java @@ -127,12 +127,6 @@ public class DelayedPayoutTxReceiverService implements DaoStateListener { burningManService.getActiveBurningManCandidates(burningManSelectionHeight) : burningManService.getBurningManCandidatesByName(burningManSelectionHeight).values(); - - if (burningManCandidates.isEmpty()) { - // If there are no compensation requests (e.g. at dev testing) we fall back to the legacy BM - return List.of(new Tuple2<>(inputAmount, burningManService.getLegacyBurningManAddress(burningManSelectionHeight))); - } - // We need to use the same txFeePerVbyte value for both traders. // We use the tradeTxFee value which is calculated from the average of taker fee tx size and deposit tx size. // Otherwise, we would need to sync the fee rate of both traders. @@ -146,12 +140,19 @@ public class DelayedPayoutTxReceiverService implements DaoStateListener { // Smallest tx size is 246. With additional change output we add 32. To be safe we use the largest expected size. double txSize = 278; long txFeePerVbyte = Math.max(DPT_MIN_TX_FEE_RATE, Math.round(tradeTxFee / txSize)); + + if (burningManCandidates.isEmpty()) { + // If there are no compensation requests (e.g. at dev testing) we fall back to the legacy BM + long spendableAmount = getSpendableAmount(1, inputAmount, txFeePerVbyte); + return List.of(new Tuple2<>(spendableAmount, burningManService.getLegacyBurningManAddress(burningManSelectionHeight))); + } + long spendableAmount = getSpendableAmount(burningManCandidates.size(), inputAmount, txFeePerVbyte); // We only use outputs > 1000 sat or at least 2 times the cost for the output (32 bytes). // If we remove outputs it will be spent as miner fee. long minOutputAmount = Math.max(DPT_MIN_OUTPUT_AMOUNT, txFeePerVbyte * 32 * 2); // Sanity check that max share of a non-legacy BM is 20% over MAX_BURN_SHARE (taking into account potential increase due adjustment) - long maxOutputAmount = Math.round(inputAmount * (BurningManService.MAX_BURN_SHARE * 1.2)); + long maxOutputAmount = Math.round(spendableAmount * (BurningManService.MAX_BURN_SHARE * 1.2)); // We accumulate small amounts which gets filtered out and subtract it from 1 to get an adjustment factor // used later to be applied to the remaining burningmen share. double adjustment = 1 - burningManCandidates.stream() From 719602358d828289fdb2d255312ea60eca8fbec7 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 5 Jan 2023 22:17:09 -0500 Subject: [PATCH 81/89] Increase pool size Signed-off-by: HenrikJannsen --- p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java index dee52c2f87..f6a93032f8 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java @@ -71,8 +71,8 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { this.peerManager = peerManager; ThreadPoolExecutor threadPoolExecutor = Utilities.getThreadPoolExecutor("Broadcaster", - maxConnections * 2, maxConnections * 3, + maxConnections * 4, 30, 30); executor = MoreExecutors.listeningDecorator(threadPoolExecutor); From 8b0f8fbeade65a9d5d073779d66c0a4291697618 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 5 Jan 2023 22:18:10 -0500 Subject: [PATCH 82/89] Cleanup Signed-off-by: HenrikJannsen --- .../java/bisq/core/dao/burningman/BtcFeeReceiverService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/BtcFeeReceiverService.java b/core/src/main/java/bisq/core/dao/burningman/BtcFeeReceiverService.java index e7f9cde77a..9134fb95af 100644 --- a/core/src/main/java/bisq/core/dao/burningman/BtcFeeReceiverService.java +++ b/core/src/main/java/bisq/core/dao/burningman/BtcFeeReceiverService.java @@ -85,7 +85,6 @@ public class BtcFeeReceiverService implements DaoStateListener { // cappedBurnAmountShare is a % value represented as double. Smallest supported value is 0.01% -> 0.0001. // By multiplying it with 10000 and using Math.floor we limit the candidate to 0.01%. // Entries with 0 will be ignored in the selection method, so we do not need to filter them out. - // List burningManCandidates = new ArrayList<>(burningManCandidatesByName.values()); int ceiling = 10000; List amountList = activeBurningManCandidates.stream() .map(BurningManCandidate::getCappedBurnAmountShare) From 9fe3d22b24a1f268aa3b88d48eb5d3e5a096c1e3 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 5 Jan 2023 22:23:24 -0500 Subject: [PATCH 83/89] Cleanup Signed-off-by: HenrikJannsen --- .../main/java/bisq/network/p2p/peers/BroadcastHandler.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java index e99fbadab3..19644fb347 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java @@ -35,6 +35,7 @@ import com.google.common.util.concurrent.SettableFuture; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.CopyOnWriteArraySet; @@ -310,8 +311,8 @@ public class BroadcastHandler implements PeerManager.Listener { if (numOfCompletedBroadcasts.get() == numOfCompletedBroadcastsTarget) { // We have heard back from 3 peers (or all peers if numPeers is lower) so we consider the message was sufficiently broadcast. broadcastRequests.stream() - .filter(broadcastRequest -> broadcastRequest.getListener() != null) .map(Broadcaster.BroadcastRequest::getListener) + .filter(Objects::nonNull) .forEach(listener -> listener.onSufficientlyBroadcast(broadcastRequests)); } else { // We check if number of open requests to peers is less than we need to reach numOfCompletedBroadcastsTarget. @@ -323,8 +324,8 @@ public class BroadcastHandler implements PeerManager.Listener { boolean timeoutAndNotEnoughSucceeded = timeoutTriggered.get() && numOfCompletedBroadcasts.get() < numOfCompletedBroadcastsTarget; if (notEnoughSucceededOrOpen || timeoutAndNotEnoughSucceeded) { broadcastRequests.stream() - .filter(broadcastRequest -> broadcastRequest.getListener() != null) .map(Broadcaster.BroadcastRequest::getListener) + .filter(Objects::nonNull) .forEach(listener -> listener.onNotSufficientlyBroadcast(numOfCompletedBroadcasts.get(), numOfFailedBroadcasts.get())); } } From 7977c8670aa420583c03633aa912ad60649f277b Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 5 Jan 2023 22:25:31 -0500 Subject: [PATCH 84/89] Cleanup Signed-off-by: HenrikJannsen --- .../bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java index 1a1512938f..4d2df54ad6 100644 --- a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java +++ b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java @@ -78,7 +78,6 @@ public class DelayedPayoutTxReceiverService implements DaoStateListener { // spike when opening arbitration. private static final long DPT_MIN_TX_FEE_RATE = 10; - private final DaoStateService daoStateService; private final BurningManService burningManService; private int currentChainHeight; From 9dc79fdcd4c0eca5adfa7641f3c2353b41a4c049 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 6 Jan 2023 10:03:39 -0500 Subject: [PATCH 85/89] Add checks if hot fix is activated for 2 other changes made since 1.9.8 Signed-off-by: HenrikJannsen --- .../core/dao/burningman/DelayedPayoutTxReceiverService.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java index 4d2df54ad6..bfe714b56f 100644 --- a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java +++ b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java @@ -71,7 +71,7 @@ public class DelayedPayoutTxReceiverService implements DaoStateListener { // than DPT_MIN_REMAINDER_TO_LEGACY_BM, otherwise we spend it as miner fee. // 25000 sat is about 5 USD @ 20k price. We use a rather high value as we want to avoid that the legacy BM // gets still payouts. - private static final long DPT_MIN_REMAINDER_TO_LEGACY_BM = 25000; + private static final long DPT_MIN_REMAINDER_TO_LEGACY_BM = isHotfixActivated() ? 25000 : 50000; // Min. fee rate for DPT. If fee rate used at take offer time was higher we use that. // We prefer a rather high fee rate to not risk that the DPT gets stuck if required fee rate would @@ -151,7 +151,9 @@ public class DelayedPayoutTxReceiverService implements DaoStateListener { // If we remove outputs it will be spent as miner fee. long minOutputAmount = Math.max(DPT_MIN_OUTPUT_AMOUNT, txFeePerVbyte * 32 * 2); // Sanity check that max share of a non-legacy BM is 20% over MAX_BURN_SHARE (taking into account potential increase due adjustment) - long maxOutputAmount = Math.round(spendableAmount * (BurningManService.MAX_BURN_SHARE * 1.2)); + long maxOutputAmount = isHotfixActivated() ? + Math.round(spendableAmount * (BurningManService.MAX_BURN_SHARE * 1.2)) : + Math.round(inputAmount * (BurningManService.MAX_BURN_SHARE * 1.2)); // We accumulate small amounts which gets filtered out and subtract it from 1 to get an adjustment factor // used later to be applied to the remaining burningmen share. double adjustment = 1 - burningManCandidates.stream() From b872683b60b80b829d67c42e60c36dfa89c585ef Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 6 Jan 2023 13:10:12 -0500 Subject: [PATCH 86/89] Use trade date for check if hotfix is activated at refund managers DPT verification Signed-off-by: HenrikJannsen --- .../burningman/DelayedPayoutTxReceiverService.java | 13 ++++++++++--- .../core/support/dispute/refund/RefundManager.java | 5 ++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java index bfe714b56f..46aedd6977 100644 --- a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java +++ b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java @@ -52,7 +52,7 @@ import static com.google.common.base.Preconditions.checkArgument; @Slf4j @Singleton public class DelayedPayoutTxReceiverService implements DaoStateListener { - private static final Date HOTFIX_ACTIVATION_DATE = Utilities.getUTCDate(2023, GregorianCalendar.JANUARY, 10); + public static final Date HOTFIX_ACTIVATION_DATE = Utilities.getUTCDate(2023, GregorianCalendar.JANUARY, 10); public static boolean isHotfixActivated() { return new Date().after(HOTFIX_ACTIVATION_DATE); @@ -121,8 +121,15 @@ public class DelayedPayoutTxReceiverService implements DaoStateListener { public List> getReceivers(int burningManSelectionHeight, long inputAmount, long tradeTxFee) { + return getReceivers(burningManSelectionHeight, inputAmount, tradeTxFee, isHotfixActivated()); + } + + public List> getReceivers(int burningManSelectionHeight, + long inputAmount, + long tradeTxFee, + boolean isHotfixActivated) { checkArgument(burningManSelectionHeight >= MIN_SNAPSHOT_HEIGHT, "Selection height must be >= " + MIN_SNAPSHOT_HEIGHT); - Collection burningManCandidates = isHotfixActivated() ? + Collection burningManCandidates = isHotfixActivated ? burningManService.getActiveBurningManCandidates(burningManSelectionHeight) : burningManService.getBurningManCandidatesByName(burningManSelectionHeight).values(); @@ -151,7 +158,7 @@ public class DelayedPayoutTxReceiverService implements DaoStateListener { // If we remove outputs it will be spent as miner fee. long minOutputAmount = Math.max(DPT_MIN_OUTPUT_AMOUNT, txFeePerVbyte * 32 * 2); // Sanity check that max share of a non-legacy BM is 20% over MAX_BURN_SHARE (taking into account potential increase due adjustment) - long maxOutputAmount = isHotfixActivated() ? + long maxOutputAmount = isHotfixActivated ? Math.round(spendableAmount * (BurningManService.MAX_BURN_SHARE * 1.2)) : Math.round(inputAmount * (BurningManService.MAX_BURN_SHARE * 1.2)); // We accumulate small amounts which gets filtered out and subtract it from 1 to get an adjustment factor diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java index 06fdc0bdfb..0ec75e3277 100644 --- a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java @@ -320,10 +320,13 @@ public final class RefundManager extends DisputeManager { Transaction depositTx = dispute.findDepositTx(btcWalletService).orElseThrow(); long inputAmount = depositTx.getOutput(0).getValue().value; int selectionHeight = dispute.getBurningManSelectionHeight(); + + boolean wasHotfixActivatedAtTradeDate = dispute.getTradeDate().after(DelayedPayoutTxReceiverService.HOTFIX_ACTIVATION_DATE); List> delayedPayoutTxReceivers = delayedPayoutTxReceiverService.getReceivers( selectionHeight, inputAmount, - dispute.getTradeTxFee()); + dispute.getTradeTxFee(), + wasHotfixActivatedAtTradeDate); log.info("Verify delayedPayoutTx using selectionHeight {} and receivers {}", selectionHeight, delayedPayoutTxReceivers); checkArgument(delayedPayoutTx.getOutputs().size() == delayedPayoutTxReceivers.size(), "Size of outputs and delayedPayoutTxReceivers must be the same"); From fa82990e6e13d69634b846a90577cc432afe5a61 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 6 Jan 2023 13:19:59 -0500 Subject: [PATCH 87/89] Handle DPT_MIN_REMAINDER_TO_LEGACY_BM with isHotfixActivated param Signed-off-by: HenrikJannsen --- .../core/dao/burningman/DelayedPayoutTxReceiverService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java index 46aedd6977..3cce568ab6 100644 --- a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java +++ b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java @@ -71,7 +71,7 @@ public class DelayedPayoutTxReceiverService implements DaoStateListener { // than DPT_MIN_REMAINDER_TO_LEGACY_BM, otherwise we spend it as miner fee. // 25000 sat is about 5 USD @ 20k price. We use a rather high value as we want to avoid that the legacy BM // gets still payouts. - private static final long DPT_MIN_REMAINDER_TO_LEGACY_BM = isHotfixActivated() ? 25000 : 50000; + private static final long DPT_MIN_REMAINDER_TO_LEGACY_BM = 25000; // Min. fee rate for DPT. If fee rate used at take offer time was higher we use that. // We prefer a rather high fee rate to not risk that the DPT gets stuck if required fee rate would @@ -189,7 +189,8 @@ public class DelayedPayoutTxReceiverService implements DaoStateListener { long available = spendableAmount - totalOutputValue; // If the available is larger than DPT_MIN_REMAINDER_TO_LEGACY_BM we send it to legacy BM // Otherwise we use it as miner fee - if (available > DPT_MIN_REMAINDER_TO_LEGACY_BM) { + long dptMinRemainderToLegacyBm = isHotfixActivated ? DPT_MIN_REMAINDER_TO_LEGACY_BM : 50000; + if (available > dptMinRemainderToLegacyBm) { receivers.add(new Tuple2<>(available, burningManService.getLegacyBurningManAddress(burningManSelectionHeight))); } } From bc5045dd2805381a075b33d368977aae036b18b2 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 6 Jan 2023 13:24:17 -0500 Subject: [PATCH 88/89] Force rebuild at CI Signed-off-by: HenrikJannsen --- .../core/dao/burningman/DelayedPayoutTxReceiverService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java index 3cce568ab6..71576ad567 100644 --- a/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java +++ b/core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java @@ -200,8 +200,8 @@ public class DelayedPayoutTxReceiverService implements DaoStateListener { private static long getSpendableAmount(int numOutputs, long inputAmount, long txFeePerVbyte) { // Output size: 32 bytes // Tx size without outputs: 51 bytes - int txSize = 51 + numOutputs * 32; // min value: txSize=83 - long minerFee = txFeePerVbyte * txSize; // min value: minerFee=830 + int txSize = 51 + numOutputs * 32; // Min value: txSize=83 + long minerFee = txFeePerVbyte * txSize; // Min value: minerFee=830 // We need to make sure we have at least 1000 sat as defined in TradeWalletService minerFee = Math.max(TradeWalletService.MIN_DELAYED_PAYOUT_TX_FEE.value, minerFee); return inputAmount - minerFee; From e9ae213e8c64e01ade69b28d16d9f41d06782e8d Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 6 Jan 2023 19:29:53 -0500 Subject: [PATCH 89/89] Avoid that at repeated onDaoStateHashesChanged calls that we show multiple popups. Signed-off-by: HenrikJannsen --- .../main/presentation/DaoPresentation.java | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/presentation/DaoPresentation.java b/desktop/src/main/java/bisq/desktop/main/presentation/DaoPresentation.java index a0d0da6afa..c35f8aaa77 100644 --- a/desktop/src/main/java/bisq/desktop/main/presentation/DaoPresentation.java +++ b/desktop/src/main/java/bisq/desktop/main/presentation/DaoPresentation.java @@ -16,6 +16,8 @@ import bisq.core.dao.state.model.blockchain.Block; import bisq.core.locale.Res; import bisq.core.user.Preferences; +import bisq.common.UserThread; + import javax.inject.Inject; import javax.inject.Singleton; @@ -30,7 +32,11 @@ import javafx.beans.value.ChangeListener; import javafx.collections.MapChangeListener; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.Nullable; + +@Slf4j @Singleton public class DaoPresentation implements DaoStateListener, DaoStateMonitoringService.Listener { public static final String DAO_NEWS = "daoNews"; @@ -47,6 +53,8 @@ public class DaoPresentation implements DaoStateListener, DaoStateMonitoringServ @Getter private final StringProperty daoStateInfo = new SimpleStringProperty(""); private final SimpleBooleanProperty showNotification = new SimpleBooleanProperty(false); + @Nullable + private Popup popup; @Inject public DaoPresentation(Preferences preferences, @@ -107,19 +115,33 @@ public class DaoPresentation implements DaoStateListener, DaoStateMonitoringServ @Override public void onDaoStateHashesChanged() { - if (!daoStateService.isParseBlockChainComplete()) { + if (!daoStateService.isParseBlockChainComplete() || bsqWalletService.getBestChainHeight() != daoStateService.getChainHeight()) { return; } - if (daoStateMonitoringService.isInConflictWithSeedNode() || - daoStateMonitoringService.isDaoStateBlockChainNotConnecting()) { - new Popup().warning(Res.get("popup.warning.daoNeedsResync")) + // We might get multiple times called onDaoStateHashesChanged. To avoid multiple popups we + // check against null and set it back to null after a 30 sec delay once the user closed it. + if (popup == null && + (daoStateMonitoringService.isInConflictWithSeedNode() || + daoStateMonitoringService.isDaoStateBlockChainNotConnecting())) { + popup = new Popup().warning(Res.get("popup.warning.daoNeedsResync")) .actionButtonTextWithGoTo("navigation.dao.networkMonitor") - .onAction(() -> navigation.navigateTo(MainView.class, DaoView.class, MonitorView.class, DaoStateMonitorView.class)) - .show(); + .onAction(() -> { + navigation.navigateTo(MainView.class, DaoView.class, MonitorView.class, DaoStateMonitorView.class); + resetPopupAndCheckAgain(); + }) + .onClose(this::resetPopupAndCheckAgain); + popup.show(); } } + private void resetPopupAndCheckAgain() { + UserThread.runAfter(() -> { + popup = null; + onDaoStateHashesChanged(); + }, 30); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Private