Merge pull request #6423 from HenrikJannsen/add-burningman-accounting

Add burningman accounting
This commit is contained in:
Christoph Atteneder 2022-12-14 10:17:38 +01:00 committed by GitHub
commit 71d6e126dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
120 changed files with 9021 additions and 441 deletions

View file

@ -129,6 +129,9 @@ public class Config {
public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation";
public static final String DAO_NODE_API_URL = "daoNodeApiUrl";
public static final String DAO_NODE_API_PORT = "daoNodeApiPort";
public static final String IS_BM_FULL_NODE = "isBmFullNode";
public static final String BM_ORACLE_NODE_PUB_KEY = "bmOracleNodePubKey";
public static final String BM_ORACLE_NODE_PRIV_KEY = "bmOracleNodePrivKey";
public static final String SEED_NODE_REPORTING_SERVER_URL = "seedNodeReportingServerUrl";
// Default values for certain options
@ -221,6 +224,9 @@ public class Config {
public final boolean bypassMempoolValidation;
public final String daoNodeApiUrl;
public final int daoNodeApiPort;
public final boolean isBmFullNode;
public final String bmOracleNodePubKey;
public final String bmOracleNodePrivKey;
public final String seedNodeReportingServerUrl;
// Properties derived from options but not exposed as options themselves
@ -681,6 +687,23 @@ public class Config {
.withRequiredArg()
.ofType(Integer.class)
.defaultsTo(8082);
ArgumentAcceptingOptionSpec<Boolean> isBmFullNode =
parser.accepts(IS_BM_FULL_NODE, "Run as Burningman full node")
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<String> bmOracleNodePubKey =
parser.accepts(BM_ORACLE_NODE_PUB_KEY, "Burningman oracle node public key")
.withRequiredArg()
.defaultsTo("");
ArgumentAcceptingOptionSpec<String> bmOracleNodePrivKey =
parser.accepts(BM_ORACLE_NODE_PRIV_KEY, "Burningman oracle node private key")
.withRequiredArg()
.defaultsTo("");
ArgumentAcceptingOptionSpec<String> seedNodeReportingServerUrlOpt =
parser.accepts(SEED_NODE_REPORTING_SERVER_URL, "URL of seed node reporting server")
.withRequiredArg()
@ -806,6 +829,9 @@ public class Config {
this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt);
this.daoNodeApiUrl = options.valueOf(daoNodeApiUrlOpt);
this.daoNodeApiPort = options.valueOf(daoNodeApiPortOpt);
this.isBmFullNode = options.valueOf(isBmFullNode);
this.bmOracleNodePubKey = options.valueOf(bmOracleNodePubKey);
this.bmOracleNodePrivKey = options.valueOf(bmOracleNodePrivKey);
this.seedNodeReportingServerUrl = options.valueOf(seedNodeReportingServerUrlOpt);
} catch (OptionException ex) {
throw new ConfigException("problem parsing option '%s': %s",

View file

@ -0,0 +1,60 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.common.util;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
public class DateUtil {
/**
*
* @param date The date which should be reset to first day of month
* @return First day in given date with time set to zero.
*/
public static Date getStartOfMonth(Date date) {
Calendar calendar = new GregorianCalendar();
calendar.setTime(date);
calendar.set(Calendar.DAY_OF_MONTH, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar.getTime();
}
/**
*
* @param year The year
* @param month The month starts with 0 for January
* @return First day in given month with time set to zero.
*/
public static Date getStartOfMonth(int year, int month) {
Calendar calendar = new GregorianCalendar();
calendar.setTime(new Date());
calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, month);
calendar.set(Calendar.DAY_OF_MONTH, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
Date time = calendar.getTime();
return time;
}
}

View file

@ -28,4 +28,8 @@ public class Hex {
public static String encode(byte[] bytes) {
return BaseEncoding.base16().lowerCase().encode(bytes);
}
public static byte[] decodeLast4Bytes(String hex) {
return decode(hex.substring(hex.length() - 8));
}
}

View file

@ -591,7 +591,7 @@ public class BsqWalletService extends WalletService implements DaoStateListener
coinSelector.setUtxoCandidates(null); // We reuse the selectors. Reset the transactionOutputCandidates field
return tx;
} catch (InsufficientMoneyException e) {
log.error("getPreparedSendTx: tx={}", tx.toString());
log.error("getPreparedSendTx: tx={}", tx);
log.error(e.toString());
throw new InsufficientBsqException(e.missing);
}

View file

@ -76,7 +76,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
public class TradeWalletService {
private static final Logger log = LoggerFactory.getLogger(TradeWalletService.class);
private static final Coin MIN_DELAYED_PAYOUT_TX_FEE = Coin.valueOf(1000);
public static final Coin MIN_DELAYED_PAYOUT_TX_FEE = Coin.valueOf(1000);
private final WalletsSetup walletsSetup;
private final Preferences preferences;
@ -696,16 +696,31 @@ public class TradeWalletService {
// Delayed payout tx
///////////////////////////////////////////////////////////////////////////////////////////
public Transaction createDelayedUnsignedPayoutTx(Transaction depositTx,
List<Tuple2<Long, String>> receivers,
long lockTime)
throws AddressFormatException, TransactionVerificationException {
TransactionOutput depositTxOutput = depositTx.getOutput(0);
Transaction delayedPayoutTx = new Transaction(params);
delayedPayoutTx.addInput(depositTxOutput);
applyLockTime(lockTime, delayedPayoutTx);
checkArgument(!receivers.isEmpty(), "receivers must not be empty");
receivers.forEach(receiver -> delayedPayoutTx.addOutput(Coin.valueOf(receiver.first), Address.fromString(params, receiver.second)));
WalletService.printTx("Unsigned delayedPayoutTx ToDonationAddress", delayedPayoutTx);
WalletService.verifyTransaction(delayedPayoutTx);
return delayedPayoutTx;
}
public Transaction createDelayedUnsignedPayoutTx(Transaction depositTx,
String donationAddressString,
Coin minerFee,
long lockTime)
throws AddressFormatException, TransactionVerificationException {
TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0);
TransactionOutput depositTxOutput = depositTx.getOutput(0);
Transaction delayedPayoutTx = new Transaction(params);
delayedPayoutTx.addInput(hashedMultiSigOutput);
delayedPayoutTx.addInput(depositTxOutput);
applyLockTime(lockTime, delayedPayoutTx);
Coin outputAmount = hashedMultiSigOutput.getValue().subtract(minerFee);
Coin outputAmount = depositTxOutput.getValue().subtract(minerFee);
delayedPayoutTx.addOutput(outputAmount, Address.fromString(params, donationAddressString));
WalletService.printTx("Unsigned delayedPayoutTx ToDonationAddress", delayedPayoutTx);
WalletService.verifyTransaction(delayedPayoutTx);

View file

@ -0,0 +1,127 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao;
import bisq.core.dao.governance.period.CycleService;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.model.governance.Cycle;
import bisq.core.dao.state.model.governance.DaoPhase;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
/**
* Utility methods for Cycle related methods.
* As they might be called often we use caching.
*/
@Slf4j
@Singleton
public class CyclesInDaoStateService {
private final DaoStateService daoStateService;
private final CycleService cycleService;
// Cached results
private final Map<Integer, Cycle> cyclesByHeight = new HashMap<>();
private final Map<Cycle, Integer> indexByCycle = new HashMap<>();
private final Map<Integer, Cycle> cyclesByIndex = new HashMap<>();
@Inject
public CyclesInDaoStateService(DaoStateService daoStateService, CycleService cycleService) {
this.daoStateService = daoStateService;
this.cycleService = cycleService;
}
public int getCycleIndexAtChainHeight(int chainHeight) {
return findCycleAtHeight(chainHeight)
.map(cycleService::getCycleIndex)
.orElse(-1);
}
public int getHeightOfFirstBlockOfResultPhaseOfPastCycle(int chainHeight, int numPastCycles) {
return findCycleAtHeight(chainHeight)
.map(cycle -> {
int cycleIndex = getIndexForCycle(cycle);
int targetIndex = Math.max(0, (cycleIndex - numPastCycles));
return getCycleAtIndex(targetIndex);
})
.map(cycle -> cycle.getFirstBlockOfPhase(DaoPhase.Phase.RESULT))
.orElse(daoStateService.getGenesisBlockHeight());
}
/**
*
* @param chainHeight Chain height from where we start
* @param numPastCycles Number of past cycles
* @return The height at the same offset from the first block of the cycle as in the current cycle minus the past cycles.
*/
public int getChainHeightOfPastCycle(int chainHeight, int numPastCycles) {
int firstBlockOfPastCycle = getHeightOfFirstBlockOfPastCycle(chainHeight, numPastCycles);
if (firstBlockOfPastCycle == daoStateService.getGenesisBlockHeight()) {
return firstBlockOfPastCycle;
}
return firstBlockOfPastCycle + getOffsetFromFirstBlockInCycle(chainHeight);
}
public Integer getOffsetFromFirstBlockInCycle(int chainHeight) {
return daoStateService.getCycle(chainHeight)
.map(c -> chainHeight - c.getHeightOfFirstBlock())
.orElse(0);
}
public int getHeightOfFirstBlockOfPastCycle(int chainHeight, int numPastCycles) {
return findCycleAtHeight(chainHeight)
.map(cycle -> getIndexForCycle(cycle) - numPastCycles)
.filter(targetIndex -> targetIndex > 0)
.map(targetIndex -> getCycleAtIndex(targetIndex).getHeightOfFirstBlock())
.orElse(daoStateService.getGenesisBlockHeight());
}
public Cycle getCycleAtIndex(int index) {
int cycleIndex = Math.max(0, index);
return Optional.ofNullable(cyclesByIndex.get(cycleIndex))
.orElseGet(() -> {
Cycle cycle = daoStateService.getCycleAtIndex(cycleIndex);
cyclesByIndex.put(cycleIndex, cycle);
return cycle;
});
}
public int getIndexForCycle(Cycle cycle) {
return Optional.ofNullable(indexByCycle.get(cycle))
.orElseGet(() -> {
int index = cycleService.getCycleIndex(cycle);
indexByCycle.put(cycle, index);
return index;
});
}
public Optional<Cycle> findCycleAtHeight(int chainHeight) {
return Optional.ofNullable(cyclesByHeight.get(chainHeight))
.or(() -> {
Optional<Cycle> optionalCycle = daoStateService.getCycle(chainHeight);
optionalCycle.ifPresent(cycle -> cyclesByHeight.put(chainHeight, cycle));
return optionalCycle;
});
}
}

View file

@ -252,11 +252,13 @@ public class DaoFacade implements DaoSetupService {
// Creation of Proposal and proposalTransaction
public ProposalWithTransaction getCompensationProposalWithTransaction(String name,
String link,
Coin requestedBsq)
Coin requestedBsq,
Optional<String> burningManReceiverAddress)
throws ProposalValidationException, InsufficientMoneyException, TxException {
return compensationProposalFactory.createProposalWithTransaction(name,
link,
requestedBsq);
requestedBsq,
burningManReceiverAddress);
}
public ProposalWithTransaction getReimbursementProposalWithTransaction(String name,

View file

@ -222,6 +222,9 @@ public class DaoModule extends AppModule {
bindConstant().annotatedWith(named(Config.RPC_BLOCK_NOTIFICATION_HOST)).to(config.rpcBlockNotificationHost);
bindConstant().annotatedWith(named(Config.DUMP_BLOCKCHAIN_DATA)).to(config.dumpBlockchainData);
bindConstant().annotatedWith(named(Config.FULL_DAO_NODE)).to(config.fullDaoNode);
bindConstant().annotatedWith(named(Config.IS_BM_FULL_NODE)).to(config.isBmFullNode);
bindConstant().annotatedWith(named(Config.BM_ORACLE_NODE_PUB_KEY)).to(config.bmOracleNodePubKey);
bindConstant().annotatedWith(named(Config.BM_ORACLE_NODE_PRIV_KEY)).to(config.bmOracleNodePrivKey);
}
}

View file

@ -17,6 +17,9 @@
package bisq.core.dao;
import bisq.core.dao.burningman.accounting.BurningManAccountingService;
import bisq.core.dao.burningman.accounting.node.AccountingNode;
import bisq.core.dao.burningman.accounting.node.AccountingNodeProvider;
import bisq.core.dao.governance.asset.AssetService;
import bisq.core.dao.governance.ballot.BallotListService;
import bisq.core.dao.governance.blindvote.BlindVoteListService;
@ -54,9 +57,11 @@ import java.util.function.Consumer;
public class DaoSetup {
private final BsqNode bsqNode;
private final List<DaoSetupService> daoSetupServices = new ArrayList<>();
private final AccountingNode accountingNode;
@Inject
public DaoSetup(BsqNodeProvider bsqNodeProvider,
AccountingNodeProvider accountingNodeProvider,
DaoStateService daoStateService,
CycleService cycleService,
BallotListService ballotListService,
@ -79,9 +84,11 @@ public class DaoSetup {
DaoStateMonitoringService daoStateMonitoringService,
ProposalStateMonitoringService proposalStateMonitoringService,
BlindVoteStateMonitoringService blindVoteStateMonitoringService,
DaoStateSnapshotService daoStateSnapshotService) {
DaoStateSnapshotService daoStateSnapshotService,
BurningManAccountingService burningManAccountingService) {
bsqNode = bsqNodeProvider.getBsqNode();
accountingNode = accountingNodeProvider.getAccountingNode();
// We need to take care of order of execution.
daoSetupServices.add(daoStateService);
@ -107,8 +114,10 @@ public class DaoSetup {
daoSetupServices.add(proposalStateMonitoringService);
daoSetupServices.add(blindVoteStateMonitoringService);
daoSetupServices.add(daoStateSnapshotService);
daoSetupServices.add(burningManAccountingService);
daoSetupServices.add(bsqNodeProvider.getBsqNode());
daoSetupServices.add(bsqNode);
daoSetupServices.add(accountingNode);
}
public void onAllServicesInitialized(Consumer<String> errorMessageHandler,
@ -116,6 +125,9 @@ public class DaoSetup {
bsqNode.setErrorMessageHandler(errorMessageHandler);
bsqNode.setWarnMessageHandler(warnMessageHandler);
accountingNode.setErrorMessageHandler(errorMessageHandler);
accountingNode.setWarnMessageHandler(warnMessageHandler);
// We add first all listeners at all services and then call the start methods.
// Some services are listening on others so we need to make sure that the
// listeners are set before we call start as that might trigger state change
@ -126,5 +138,6 @@ public class DaoSetup {
public void shutDown() {
bsqNode.shutDown();
accountingNode.shutDown();
}
}

View file

@ -0,0 +1,132 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman;
import bisq.core.dao.burningman.model.BurningManCandidate;
import bisq.core.dao.state.DaoStateListener;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.model.blockchain.Block;
import javax.inject.Inject;
import javax.inject.Singleton;
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;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Singleton
public class BtcFeeReceiverService implements DaoStateListener {
private final BurningManService burningManService;
private int currentChainHeight;
@Inject
public BtcFeeReceiverService(DaoStateService daoStateService, BurningManService burningManService) {
this.burningManService = burningManService;
daoStateService.addDaoStateListener(this);
daoStateService.getLastBlock().ifPresent(this::applyBlock);
}
///////////////////////////////////////////////////////////////////////////////////////////
// DaoStateListener
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onParseBlockCompleteAfterBatchProcessing(Block block) {
applyBlock(block);
}
private void applyBlock(Block block) {
currentChainHeight = block.getHeight();
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public String getAddress() {
if (!BurningManService.isActivated()) {
// Before activation, we fall back to the current fee receiver address
return BurningManPresentationService.LEGACY_BURNING_MAN_BTC_FEES_ADDRESS;
}
Map<String, BurningManCandidate> burningManCandidatesByName = burningManService.getBurningManCandidatesByName(currentChainHeight);
if (burningManCandidatesByName.isEmpty()) {
// If there are no compensation requests (e.g. at dev testing) we fall back to the default address
return burningManService.getLegacyBurningManAddress(currentChainHeight);
}
// It might be that we do not reach 100% if some entries had a cappedBurnAmountShare.
// In that case we fill up the gap to 100% with the legacy BM.
// 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<BurningManCandidate> burningManCandidates = new ArrayList<>(burningManCandidatesByName.values());
int ceiling = 10000;
List<Long> amountList = burningManCandidates.stream()
.map(BurningManCandidate::getCappedBurnAmountShare)
.map(cappedBurnAmountShare -> (long) Math.floor(cappedBurnAmountShare * ceiling))
.collect(Collectors.toList());
long sum = amountList.stream().mapToLong(e -> e).sum();
// If we have not reached the 100% we fill the missing gap with the legacy BM
if (sum < ceiling) {
amountList.add(ceiling - sum);
}
int winnerIndex = getRandomIndex(amountList, new Random());
if (winnerIndex == burningManCandidates.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()
.orElse(burningManService.getLegacyBurningManAddress(currentChainHeight));
}
@VisibleForTesting
static int getRandomIndex(List<Long> weights, Random random) {
long sum = weights.stream().mapToLong(n -> n).sum();
if (sum == 0) {
return -1;
}
long target = random.longs(0, sum).findFirst().orElseThrow() + 1;
return findIndex(weights, target);
}
@VisibleForTesting
static int findIndex(List<Long> weights, long target) {
int currentRange = 0;
for (int i = 0; i < weights.size(); i++) {
currentRange += weights.get(i);
if (currentRange >= target) {
return i;
}
}
return 0;
}
}

View file

@ -0,0 +1,252 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman;
import bisq.core.dao.CyclesInDaoStateService;
import bisq.core.dao.burningman.model.BurnOutputModel;
import bisq.core.dao.burningman.model.BurningManCandidate;
import bisq.core.dao.burningman.model.ReimbursementModel;
import bisq.core.dao.governance.param.Param;
import bisq.core.dao.governance.proposal.ProposalService;
import bisq.core.dao.governance.proposal.storage.appendonly.ProposalPayload;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.model.blockchain.Tx;
import bisq.core.dao.state.model.governance.Cycle;
import bisq.core.dao.state.model.governance.Issuance;
import bisq.core.dao.state.model.governance.IssuanceType;
import bisq.core.dao.state.model.governance.ReimbursementProposal;
import bisq.common.config.Config;
import bisq.common.util.Hex;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
/**
* Burn target related API. Not touching trade protocol aspects and parameters can be changed here without risking to
* break trade protocol validations.
*/
@Slf4j
@Singleton
class BurnTargetService {
// Number of cycles for accumulating reimbursement amounts. Used for the burn target.
private static final int NUM_CYCLES_BURN_TARGET = 12;
private static final int NUM_CYCLES_AVERAGE_DISTRIBUTION = 3;
// 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 final DaoStateService daoStateService;
private final CyclesInDaoStateService cyclesInDaoStateService;
private final ProposalService proposalService;
@Inject
public BurnTargetService(DaoStateService daoStateService,
CyclesInDaoStateService cyclesInDaoStateService,
ProposalService proposalService) {
this.daoStateService = daoStateService;
this.cyclesInDaoStateService = cyclesInDaoStateService;
this.proposalService = proposalService;
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
Set<ReimbursementModel> getReimbursements(int chainHeight) {
Set<ReimbursementModel> reimbursements = new HashSet<>();
daoStateService.getIssuanceSetForType(IssuanceType.REIMBURSEMENT).stream()
.filter(issuance -> issuance.getChainHeight() <= chainHeight)
.forEach(issuance -> getReimbursementProposalsForIssuance(issuance)
.forEach(reimbursementProposal -> {
int issuanceHeight = issuance.getChainHeight();
long issuanceAmount = issuance.getAmount();
long issuanceDate = daoStateService.getBlockTime(issuanceHeight);
int cycleIndex = cyclesInDaoStateService.getCycleIndexAtChainHeight(issuanceHeight);
reimbursements.add(new ReimbursementModel(
issuanceAmount,
issuanceHeight,
issuanceDate,
cycleIndex,
reimbursementProposal.getTxId()));
}));
return reimbursements;
}
long getBurnTarget(int chainHeight, Collection<BurningManCandidate> burningManCandidates) {
// Reimbursements are taken into account at result vote block
int chainHeightOfPastCycle = cyclesInDaoStateService.getChainHeightOfPastCycle(chainHeight, NUM_CYCLES_BURN_TARGET);
long accumulatedReimbursements = getAccumulatedReimbursements(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);
long accumulatedEstimatedBtcTradeFees = getAccumulatedEstimatedBtcTradeFees(chainHeight, heightOfFirstBlockOfPastCycle);
// Legacy BurningMan
Set<Tx> proofOfBurnTxs = getProofOfBurnTxs(chainHeight, chainHeightOfPastCycle);
long burnedAmountFromLegacyBurningManDPT = getBurnedAmountFromLegacyBurningManDPT(proofOfBurnTxs, chainHeight, chainHeightOfPastCycle);
long burnedAmountFromLegacyBurningMansBtcFees = getBurnedAmountFromLegacyBurningMansBtcFees(proofOfBurnTxs, chainHeight, chainHeightOfPastCycle);
// Distributed BurningMen
long burnedAmountFromBurningMen = getBurnedAmountFromBurningMen(burningManCandidates, chainHeight, chainHeightOfPastCycle);
long burnTarget = accumulatedReimbursements
+ accumulatedEstimatedBtcTradeFees
- burnedAmountFromLegacyBurningManDPT
- burnedAmountFromLegacyBurningMansBtcFees
- burnedAmountFromBurningMen;
log.info("accumulatedReimbursements: {}\n" +
"+ accumulatedEstimatedBtcTradeFees: {}\n" +
"- burnedAmountFromLegacyBurningManDPT: {}\n" +
"- burnedAmountFromLegacyBurningMansBtcFees: {}\n" +
"- burnedAmountFromBurningMen: {}\n" +
"= burnTarget: {}\n",
accumulatedReimbursements,
accumulatedEstimatedBtcTradeFees,
burnedAmountFromLegacyBurningManDPT,
burnedAmountFromLegacyBurningMansBtcFees,
burnedAmountFromBurningMen,
burnTarget);
return burnTarget;
}
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);
// Param changes are taken into account at first block at next cycle after voting
int firstBlockOfPastCycle = cyclesInDaoStateService.getHeightOfFirstBlockOfPastCycle(chainHeight, NUM_CYCLES_AVERAGE_DISTRIBUTION - 1);
long btcTradeFees = getAccumulatedEstimatedBtcTradeFees(chainHeight, firstBlockOfPastCycle);
return Math.round((reimbursements + btcTradeFees) / (double) NUM_CYCLES_AVERAGE_DISTRIBUTION);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private Stream<ReimbursementProposal> getReimbursementProposalsForIssuance(Issuance issuance) {
return proposalService.getProposalPayloads().stream()
.map(ProposalPayload::getProposal)
.filter(proposal -> issuance.getTxId().equals(proposal.getTxId()))
.filter(proposal -> proposal instanceof ReimbursementProposal)
.map(proposal -> (ReimbursementProposal) proposal);
}
private long getAccumulatedReimbursements(int chainHeight, int fromBlock) {
return getReimbursements(chainHeight).stream()
.filter(reimbursementModel -> reimbursementModel.getHeight() > fromBlock)
.filter(reimbursementModel -> reimbursementModel.getHeight() <= chainHeight)
.mapToLong(ReimbursementModel::getAmount)
.sum();
}
// The BTC fees are set by parameter and becomes active at first block of the next cycle after voting.
private long getAccumulatedEstimatedBtcTradeFees(int chainHeight, int fromBlock) {
return daoStateService.getCycles().stream()
.filter(cycle -> cycle.getHeightOfFirstBlock() >= fromBlock)
.filter(cycle -> cycle.getHeightOfFirstBlock() <= chainHeight)
.mapToLong(this::getBtcTradeFeeFromParam)
.sum();
}
private long getBtcTradeFeeFromParam(Cycle cycle) {
int value = daoStateService.getParamValueAsBlock(Param.LOCK_TIME_TRADE_PAYOUT, cycle.getHeightOfFirstBlock());
// Ignore default value (4320)
return value != 4320 ? value : DEFAULT_ESTIMATED_BTC_TRADE_FEE_REVENUE_PER_CYCLE;
}
private Set<Tx> getProofOfBurnTxs(int chainHeight, int fromBlock) {
return daoStateService.getProofOfBurnTxs().stream()
.filter(tx -> tx.getBlockHeight() > fromBlock)
.filter(tx -> tx.getBlockHeight() <= chainHeight)
.collect(Collectors.toSet());
}
private long getBurnedAmountFromLegacyBurningManDPT(Set<Tx> proofOfBurnTxs, int chainHeight, int fromBlock) {
// Legacy burningman use those opReturn data to mark their burn transactions from delayed payout transaction cases.
// opReturn data from delayed payout txs when BM traded with the refund agent: 1701e47e5d8030f444c182b5e243871ebbaeadb5e82f
// opReturn data from delayed payout txs when BM traded with traders who got reimbursed by the DAO: 1701293c488822f98e70e047012f46f5f1647f37deb7
return proofOfBurnTxs.stream()
.filter(tx -> tx.getBlockHeight() > fromBlock)
.filter(tx -> tx.getBlockHeight() <= chainHeight)
.filter(tx -> {
String hash = Hex.encode(tx.getLastTxOutput().getOpReturnData());
return "1701e47e5d8030f444c182b5e243871ebbaeadb5e82f".equals(hash) ||
"1701293c488822f98e70e047012f46f5f1647f37deb7".equals(hash);
})
.mapToLong(Tx::getBurntBsq)
.sum();
}
private long getBurnedAmountFromLegacyBurningMansBtcFees(Set<Tx> proofOfBurnTxs, int chainHeight, int fromBlock) {
// Legacy burningman use the below opReturn data to mark their burn transactions from Btc trade fees.
return proofOfBurnTxs.stream()
.filter(tx -> tx.getBlockHeight() > fromBlock)
.filter(tx -> tx.getBlockHeight() <= chainHeight)
.filter(tx -> "1701721206fe6b40777763de1c741f4fd2706d94775d".equals(Hex.encode(tx.getLastTxOutput().getOpReturnData())))
.mapToLong(Tx::getBurntBsq)
.sum();
}
private long getBurnedAmountFromBurningMen(Collection<BurningManCandidate> burningManCandidates,
int chainHeight,
int fromBlock) {
return burningManCandidates.stream()
.map(burningManCandidate -> burningManCandidate.getBurnOutputModels().stream()
.filter(burnOutputModel -> burnOutputModel.getHeight() > fromBlock)
.filter(burnOutputModel -> burnOutputModel.getHeight() <= chainHeight)
.mapToLong(BurnOutputModel::getAmount)
.sum())
.mapToLong(e -> e)
.sum();
}
long getAccumulatedDecayedBurnedAmount(Collection<BurningManCandidate> burningManCandidates, int chainHeight) {
int fromBlock = cyclesInDaoStateService.getChainHeightOfPastCycle(chainHeight, NUM_CYCLES_BURN_TARGET);
return getAccumulatedDecayedBurnedAmount(burningManCandidates, chainHeight, fromBlock);
}
private long getAccumulatedDecayedBurnedAmount(Collection<BurningManCandidate> burningManCandidates,
int chainHeight,
int fromBlock) {
return burningManCandidates.stream()
.map(burningManCandidate -> burningManCandidate.getBurnOutputModels().stream()
.filter(burnOutputModel -> burnOutputModel.getHeight() > fromBlock)
.filter(burnOutputModel -> burnOutputModel.getHeight() <= chainHeight)
.mapToLong(BurnOutputModel::getDecayedAmount)
.sum())
.mapToLong(e -> e)
.sum();
}
}

View file

@ -0,0 +1,366 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman;
import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.dao.CyclesInDaoStateService;
import bisq.core.dao.burningman.model.BurnOutputModel;
import bisq.core.dao.burningman.model.BurningManCandidate;
import bisq.core.dao.burningman.model.LegacyBurningMan;
import bisq.core.dao.burningman.model.ReimbursementModel;
import bisq.core.dao.governance.proposal.MyProposalListService;
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.Block;
import bisq.core.dao.state.model.blockchain.Tx;
import bisq.core.dao.state.model.blockchain.TxOutput;
import bisq.core.dao.state.model.governance.CompensationProposal;
import bisq.core.dao.state.model.governance.Proposal;
import bisq.network.p2p.storage.P2PDataStorage;
import bisq.common.config.Config;
import bisq.common.util.Hex;
import bisq.common.util.Tuple2;
import javax.inject.Inject;
import javax.inject.Singleton;
import com.google.common.annotations.VisibleForTesting;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
/**
* Provides APIs for burningman data representation in the UI.
*/
@Slf4j
@Singleton
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;
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";
// 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<String> 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<String> OP_RETURN_DATA_LEGACY_BM_FEES = Config.baseCurrencyNetwork().isRegtest() ?
Set.of("1701b3253b7b92bb7f0916b05f10d4fa92be8e48f5e6") :
Set.of("1701721206fe6b40777763de1c741f4fd2706d94775d");
private final DaoStateService daoStateService;
private final CyclesInDaoStateService cyclesInDaoStateService;
private final MyProposalListService myProposalListService;
private final BsqWalletService bsqWalletService;
private final BurningManService burningManService;
private final BurnTargetService burnTargetService;
private int currentChainHeight;
private Optional<Long> burnTarget = Optional.empty();
private final Map<String, BurningManCandidate> burningManCandidatesByName = new HashMap<>();
private final Set<ReimbursementModel> reimbursements = new HashSet<>();
private Optional<Long> averageDistributionPerCycle = Optional.empty();
private Set<String> myCompensationRequestNames = null;
@SuppressWarnings("OptionalAssignedToNull")
private Optional<Set<String>> myGenesisOutputNames = null;
private Optional<LegacyBurningMan> legacyBurningManDPT = Optional.empty();
private Optional<LegacyBurningMan> legacyBurningManBtcFees = Optional.empty();
private final Map<P2PDataStorage.ByteArray, Set<TxOutput>> proofOfBurnOpReturnTxOutputByHash = new HashMap<>();
private final Map<String, String> burningManNameByAddress = new HashMap<>();
@Inject
public BurningManPresentationService(DaoStateService daoStateService,
CyclesInDaoStateService cyclesInDaoStateService,
MyProposalListService myProposalListService,
BsqWalletService bsqWalletService,
BurningManService burningManService,
BurnTargetService burnTargetService) {
this.daoStateService = daoStateService;
this.cyclesInDaoStateService = cyclesInDaoStateService;
this.myProposalListService = myProposalListService;
this.bsqWalletService = bsqWalletService;
this.burningManService = burningManService;
this.burnTargetService = burnTargetService;
daoStateService.addDaoStateListener(this);
daoStateService.getLastBlock().ifPresent(this::applyBlock);
}
///////////////////////////////////////////////////////////////////////////////////////////
// DaoStateListener
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onParseBlockCompleteAfterBatchProcessing(Block block) {
applyBlock(block);
}
private void applyBlock(Block block) {
currentChainHeight = block.getHeight();
burningManCandidatesByName.clear();
reimbursements.clear();
burnTarget = Optional.empty();
myCompensationRequestNames = null;
averageDistributionPerCycle = Optional.empty();
legacyBurningManDPT = Optional.empty();
legacyBurningManBtcFees = Optional.empty();
proofOfBurnOpReturnTxOutputByHash.clear();
burningManNameByAddress.clear();
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public long getBurnTarget() {
if (burnTarget.isPresent()) {
return burnTarget.get();
}
burnTarget = Optional.of(burnTargetService.getBurnTarget(currentChainHeight, getBurningManCandidatesByName().values()));
return burnTarget.get();
}
public long getBoostedBurnTarget() {
return getBurnTarget() + BURN_TARGET_BOOST_AMOUNT;
}
public long getAverageDistributionPerCycle() {
if (averageDistributionPerCycle.isPresent()) {
return averageDistributionPerCycle.get();
}
averageDistributionPerCycle = Optional.of(burnTargetService.getAverageDistributionPerCycle(currentChainHeight));
return averageDistributionPerCycle.get();
}
public long getExpectedRevenue(BurningManCandidate burningManCandidate) {
return Math.round(burningManCandidate.getCappedBurnAmountShare() * getAverageDistributionPerCycle());
}
public Tuple2<Long, Long> getCandidateBurnTarget(BurningManCandidate burningManCandidate) {
long burnTarget = getBurnTarget();
double compensationShare = burningManCandidate.getCompensationShare();
if (burnTarget == 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);
long totalBurnedAmount = burnTargetService.getAccumulatedDecayedBurnedAmount(getBurningManCandidatesByName().values(), currentChainHeight);
if (totalBurnedAmount == 0) {
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);
// If below dust we set value to 0
myBurnAmount = myBurnAmount < 546 ? 0 : myBurnAmount;
return new Tuple2<>(myBurnAmount, myMaxBurnAmount);
} else {
// We have reached our cap.
return new Tuple2<>(0L, MAX_BURN_TARGET_LOWER_FLOOR);
}
}
@VisibleForTesting
static long getMissingAmountToReachTargetShare(long total, long myAmount, double myTargetShare) {
long others = total - myAmount;
double shareTargetOthers = 1 - myTargetShare;
double targetAmount = shareTargetOthers > 0 ? myTargetShare / shareTargetOthers * others : 0;
return Math.round(targetAmount) - myAmount;
}
public Set<ReimbursementModel> getReimbursements() {
if (!reimbursements.isEmpty()) {
return reimbursements;
}
reimbursements.addAll(burnTargetService.getReimbursements(currentChainHeight));
return reimbursements;
}
public Optional<Set<String>> findMyGenesisOutputNames() {
// Optional.empty is valid case, so we use null to detect if it was set.
// As it does not change at new blocks its only set once.
//noinspection OptionalAssignedToNull
if (myGenesisOutputNames != null) {
return myGenesisOutputNames;
}
myGenesisOutputNames = daoStateService.getGenesisTx()
.flatMap(tx -> Optional.ofNullable(bsqWalletService.getTransaction(tx.getId()))
.map(genesisTransaction -> genesisTransaction.getOutputs().stream()
.filter(transactionOutput -> transactionOutput.isMine(bsqWalletService.getWallet()))
.map(transactionOutput -> BurningManService.GENESIS_OUTPUT_PREFIX + transactionOutput.getIndex())
.collect(Collectors.toSet())
)
);
return myGenesisOutputNames;
}
public Set<String> getMyCompensationRequestNames() {
// Can be empty, so we compare with null and reset to null at new block
if (myCompensationRequestNames != null) {
return myCompensationRequestNames;
}
myCompensationRequestNames = myProposalListService.getList().stream()
.filter(proposal -> proposal instanceof CompensationProposal)
.map(Proposal::getName)
.collect(Collectors.toSet());
return myCompensationRequestNames;
}
public Map<String, BurningManCandidate> getBurningManCandidatesByName() {
// Cached value is only used for currentChainHeight
if (!burningManCandidatesByName.isEmpty()) {
return burningManCandidatesByName;
}
burningManCandidatesByName.putAll(burningManService.getBurningManCandidatesByName(currentChainHeight));
return burningManCandidatesByName;
}
public LegacyBurningMan getLegacyBurningManForDPT() {
if (legacyBurningManDPT.isPresent()) {
return legacyBurningManDPT.get();
}
// We do not add the legacy burningman to the list but keep it as class field only to avoid that it
// interferes with usage of the burningManCandidatesByName map.
LegacyBurningMan legacyBurningManDPT = getLegacyBurningMan(burningManService.getLegacyBurningManAddress(currentChainHeight), OP_RETURN_DATA_LEGACY_BM_DPT);
this.legacyBurningManDPT = Optional.of(legacyBurningManDPT);
return legacyBurningManDPT;
}
public LegacyBurningMan getLegacyBurningManForBtcFees() {
if (legacyBurningManBtcFees.isPresent()) {
return legacyBurningManBtcFees.get();
}
// We do not add the legacy burningman to the list but keep it as class field only to avoid that it
// interferes with usage of the burningManCandidatesByName map.
LegacyBurningMan legacyBurningManBtcFees = getLegacyBurningMan(LEGACY_BURNING_MAN_BTC_FEES_ADDRESS, OP_RETURN_DATA_LEGACY_BM_FEES);
this.legacyBurningManBtcFees = Optional.of(legacyBurningManBtcFees);
return legacyBurningManBtcFees;
}
private LegacyBurningMan getLegacyBurningMan(String address, Set<String> opReturnData) {
LegacyBurningMan legacyBurningMan = new LegacyBurningMan(address);
// The opReturnData used by legacy BM at burning BSQ.
getProofOfBurnOpReturnTxOutputByHash().values().stream()
.flatMap(txOutputs -> txOutputs.stream()
.filter(txOutput -> {
String opReturnAsHex = Hex.encode(txOutput.getOpReturnData());
return opReturnData.stream().anyMatch(e -> e.equals(opReturnAsHex));
}))
.forEach(burnOutput -> {
int burnOutputHeight = burnOutput.getBlockHeight();
Optional<Tx> optionalTx = daoStateService.getTx(burnOutput.getTxId());
long burnOutputAmount = optionalTx.map(Tx::getBurntBsq).orElse(0L);
long date = optionalTx.map(BaseTx::getTime).orElse(0L);
int cycleIndex = cyclesInDaoStateService.getCycleIndexAtChainHeight(burnOutputHeight);
legacyBurningMan.addBurnOutputModel(new BurnOutputModel(burnOutputAmount,
burnOutputAmount,
burnOutputHeight,
burnOutput.getTxId(),
date,
cycleIndex));
});
// Set remaining share if the sum of all capped shares does not reach 100%.
double burnAmountShareOfOthers = getBurningManCandidatesByName().values().stream()
.mapToDouble(BurningManCandidate::getCappedBurnAmountShare)
.sum();
legacyBurningMan.applyBurnAmountShare(1 - burnAmountShareOfOthers);
return legacyBurningMan;
}
public Map<String, String> getBurningManNameByAddress() {
if (!burningManNameByAddress.isEmpty()) {
return burningManNameByAddress;
}
// clone to not alter source map. We do not store legacy BM in the source map.
Map<String, BurningManCandidate> burningManCandidatesByName = new HashMap<>(getBurningManCandidatesByName());
burningManCandidatesByName.put(LEGACY_BURNING_MAN_DPT_NAME, getLegacyBurningManForDPT());
burningManCandidatesByName.put(LEGACY_BURNING_MAN_BTC_FEES_NAME, getLegacyBurningManForBtcFees());
Map<String, Set<String>> receiverAddressesByBurningManName = new HashMap<>();
burningManCandidatesByName.forEach((name, burningManCandidate) -> {
receiverAddressesByBurningManName.putIfAbsent(name, new HashSet<>());
receiverAddressesByBurningManName.get(name).addAll(burningManCandidate.getAllAddresses());
});
Map<String, String> map = new HashMap<>();
receiverAddressesByBurningManName
.forEach((name, addresses) -> addresses
.forEach(address -> map.putIfAbsent(address, name)));
burningManNameByAddress.putAll(map);
return burningManNameByAddress;
}
public String getGenesisTxId() {
return daoStateService.getGenesisTxId();
}
private Map<P2PDataStorage.ByteArray, Set<TxOutput>> getProofOfBurnOpReturnTxOutputByHash() {
if (!proofOfBurnOpReturnTxOutputByHash.isEmpty()) {
return proofOfBurnOpReturnTxOutputByHash;
}
proofOfBurnOpReturnTxOutputByHash.putAll(burningManService.getProofOfBurnOpReturnTxOutputByHash(currentChainHeight));
return proofOfBurnOpReturnTxOutputByHash;
}
}

View file

@ -0,0 +1,330 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman;
import bisq.core.dao.CyclesInDaoStateService;
import bisq.core.dao.burningman.model.BurnOutputModel;
import bisq.core.dao.burningman.model.BurningManCandidate;
import bisq.core.dao.burningman.model.CompensationModel;
import bisq.core.dao.governance.param.Param;
import bisq.core.dao.governance.proofofburn.ProofOfBurnConsensus;
import bisq.core.dao.governance.proposal.ProposalService;
import bisq.core.dao.governance.proposal.storage.appendonly.ProposalPayload;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.model.blockchain.BaseTx;
import bisq.core.dao.state.model.blockchain.Tx;
import bisq.core.dao.state.model.blockchain.TxOutput;
import bisq.core.dao.state.model.governance.CompensationProposal;
import bisq.core.dao.state.model.governance.Issuance;
import bisq.core.dao.state.model.governance.IssuanceType;
import bisq.network.p2p.storage.P2PDataStorage;
import bisq.common.config.Config;
import bisq.common.util.Utilities;
import javax.inject.Inject;
import javax.inject.Singleton;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
/**
* Methods are used by the DelayedPayoutTxReceiverService, which is used in the trade protocol for creating and
* verifying the delayed payout transaction. As verification is done by trade peer it requires data to be deterministic.
* Parameters listed here must not be changed as they could break verification of the peers
* delayed payout transaction in case not both traders are using the same version.
*/
@Slf4j
@Singleton
public class BurningManService {
private static final Date ACTIVATION_DATE = Utilities.getUTCDate(2023, GregorianCalendar.JANUARY, 1);
public static boolean isActivated() {
return Config.baseCurrencyNetwork().isRegtest() || new Date().after(ACTIVATION_DATE);
}
// Parameters
// Cannot be changed after release as it would break trade protocol verification of DPT receivers.
// Prefix for generic names for the genesis outputs. Appended with output index.
// Used as pre-image for burning.
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;
// 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;
// The number of cycles we go back for the decay function used for burned amounts.
private static final int NUM_CYCLES_BURN_AMOUNT_DECAY = 12;
// 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;
// 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
// lose their deposit and cannot gain more than 11% of the DPT payout. As the total amount in a trade is 2 times
// that deposit plus the trade amount the limiting factor here is 11% (0.15 / 1.3).
public static final double MAX_BURN_SHARE = 0.11;
private final DaoStateService daoStateService;
private final CyclesInDaoStateService cyclesInDaoStateService;
private final ProposalService proposalService;
@Inject
public BurningManService(DaoStateService daoStateService,
CyclesInDaoStateService cyclesInDaoStateService,
ProposalService proposalService) {
this.daoStateService = daoStateService;
this.cyclesInDaoStateService = cyclesInDaoStateService;
this.proposalService = proposalService;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Package scope API
///////////////////////////////////////////////////////////////////////////////////////////
Map<String, BurningManCandidate> getBurningManCandidatesByName(int chainHeight) {
Map<String, BurningManCandidate> burningManCandidatesByName = new HashMap<>();
Map<P2PDataStorage.ByteArray, Set<TxOutput>> proofOfBurnOpReturnTxOutputByHash = getProofOfBurnOpReturnTxOutputByHash(chainHeight);
// Add contributors who made a compensation request
daoStateService.getIssuanceSetForType(IssuanceType.COMPENSATION).stream()
.filter(issuance -> issuance.getChainHeight() <= chainHeight)
.forEach(issuance -> {
getCompensationProposalsForIssuance(issuance).forEach(compensationProposal -> {
String name = compensationProposal.getName();
burningManCandidatesByName.putIfAbsent(name, new BurningManCandidate());
BurningManCandidate candidate = burningManCandidatesByName.get(name);
// Issuance
compensationProposal.getBurningManReceiverAddress()
.or(() -> daoStateService.getTx(compensationProposal.getTxId())
.map(this::getAddressFromCompensationRequest))
.ifPresent(address -> {
int issuanceHeight = issuance.getChainHeight();
long issuanceAmount = getIssuanceAmountForCompensationRequest(issuance);
int cycleIndex = cyclesInDaoStateService.getCycleIndexAtChainHeight(issuanceHeight);
if (isValidCompensationRequest(name, cycleIndex, issuanceAmount)) {
long decayedIssuanceAmount = getDecayedCompensationAmount(issuanceAmount, issuanceHeight, chainHeight);
long issuanceDate = daoStateService.getBlockTime(issuanceHeight);
candidate.addCompensationModel(CompensationModel.fromCompensationRequest(address,
issuanceAmount,
decayedIssuanceAmount,
issuanceHeight,
issuance.getTxId(),
issuanceDate,
cycleIndex));
}
});
addBurnOutputModel(chainHeight, proofOfBurnOpReturnTxOutputByHash, name, candidate);
});
}
);
// Add output receivers of genesis transaction
daoStateService.getGenesisTx()
.ifPresent(tx -> tx.getTxOutputs().forEach(txOutput -> {
String name = GENESIS_OUTPUT_PREFIX + txOutput.getIndex();
burningManCandidatesByName.putIfAbsent(name, new BurningManCandidate());
BurningManCandidate candidate = burningManCandidatesByName.get(name);
// Issuance
int issuanceHeight = txOutput.getBlockHeight();
long issuanceAmount = txOutput.getValue();
long decayedAmount = getDecayedGenesisOutputAmount(issuanceAmount);
long issuanceDate = daoStateService.getBlockTime(issuanceHeight);
candidate.addCompensationModel(CompensationModel.fromGenesisOutput(txOutput.getAddress(),
issuanceAmount,
decayedAmount,
issuanceHeight,
txOutput.getTxId(),
txOutput.getIndex(),
issuanceDate));
addBurnOutputModel(chainHeight, proofOfBurnOpReturnTxOutputByHash, name, candidate);
}));
Collection<BurningManCandidate> burningManCandidates = burningManCandidatesByName.values();
double totalDecayedCompensationAmounts = burningManCandidates.stream()
.mapToDouble(BurningManCandidate::getAccumulatedDecayedCompensationAmount)
.sum();
double totalDecayedBurnAmounts = burningManCandidates.stream()
.mapToDouble(BurningManCandidate::getAccumulatedDecayedBurnAmount)
.sum();
burningManCandidates.forEach(candidate -> candidate.calculateShares(totalDecayedCompensationAmounts, totalDecayedBurnAmounts));
return burningManCandidatesByName;
}
String getLegacyBurningManAddress(int chainHeight) {
return daoStateService.getParamValue(Param.RECIPIENT_BTC_ADDRESS, chainHeight);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
Map<P2PDataStorage.ByteArray, Set<TxOutput>> getProofOfBurnOpReturnTxOutputByHash(int chainHeight) {
Map<P2PDataStorage.ByteArray, Set<TxOutput>> map = new HashMap<>();
daoStateService.getProofOfBurnOpReturnTxOutputs().stream()
.filter(txOutput -> txOutput.getBlockHeight() <= chainHeight)
.forEach(txOutput -> {
P2PDataStorage.ByteArray key = new P2PDataStorage.ByteArray(ProofOfBurnConsensus.getHashFromOpReturnData(txOutput.getOpReturnData()));
map.putIfAbsent(key, new HashSet<>());
map.get(key).add(txOutput);
});
return map;
}
private Stream<CompensationProposal> getCompensationProposalsForIssuance(Issuance issuance) {
return proposalService.getProposalPayloads().stream()
.map(ProposalPayload::getProposal)
.filter(proposal -> issuance.getTxId().equals(proposal.getTxId()))
.filter(proposal -> proposal instanceof CompensationProposal)
.map(proposal -> (CompensationProposal) proposal);
}
private String getAddressFromCompensationRequest(Tx tx) {
ImmutableList<TxOutput> txOutputs = tx.getTxOutputs();
// The compensation request tx has usually 4 outputs. If there is no BTC change its 3 outputs.
// BTC change output is at index 2 if present otherwise
// we use the BSQ address of the compensation candidate output at index 1.
// See https://docs.bisq.network/dao-technical-overview.html#compensation-request-txreimbursement-request-tx
if (txOutputs.size() == 4) {
return txOutputs.get(2).getAddress();
} else {
return txOutputs.get(1).getAddress();
}
}
private long getIssuanceAmountForCompensationRequest(Issuance issuance) {
// There was a reimbursement for a conference sponsorship with 44776 BSQ. We remove that as well.
// See https://github.com/bisq-network/compensation/issues/498
if (issuance.getTxId().equals("01455fc4c88fca0665a5f56a90ff03fb9e3e88c3430ffc5217246e32d180aa64")) {
return 119400; // That was the compensation part
} else {
return issuance.getAmount();
}
}
private boolean isValidCompensationRequest(String name, int cycleIndex, long issuanceAmount) {
// Up to cycle 15 the RefundAgent made reimbursement requests as compensation requests. We filter out those entries.
// As it is mixed with RefundAgents real compensation requests we take out all above 3500 BSQ.
boolean isReimbursementOfRefundAgent = name.equals("RefundAgent") && cycleIndex <= 15 && issuanceAmount > 350000;
return !isReimbursementOfRefundAgent;
}
private long getDecayedCompensationAmount(long amount, int issuanceHeight, int chainHeight) {
int chainHeightOfPastCycle = cyclesInDaoStateService.getChainHeightOfPastCycle(chainHeight, NUM_CYCLES_COMP_REQUEST_DECAY);
return getDecayedAmount(amount, issuanceHeight, chainHeight, chainHeightOfPastCycle);
}
// Linear decay between currentBlockHeight (100% of amount) and issuanceHeight
// chainHeightOfPastCycle is currentBlockHeight - numCycles*cycleDuration. It changes with each block and
// distance to currentBlockHeight is the same if cycle durations have not changed (possible via DAo voting but never done).
@VisibleForTesting
static long getDecayedAmount(long amount,
int issuanceHeight,
int currentBlockHeight,
int chainHeightOfPastCycle) {
if (issuanceHeight > currentBlockHeight)
throw new IllegalArgumentException("issuanceHeight must not be larger than currentBlockHeight. issuanceHeight=" + issuanceHeight + "; currentBlockHeight=" + currentBlockHeight);
if (currentBlockHeight < 0)
throw new IllegalArgumentException("currentBlockHeight must not be negative. currentBlockHeight=" + currentBlockHeight);
if (amount < 0)
throw new IllegalArgumentException("amount must not be negative. amount" + amount);
if (issuanceHeight < 0)
throw new IllegalArgumentException("issuanceHeight must not be negative. issuanceHeight=" + issuanceHeight);
if (currentBlockHeight <= chainHeightOfPastCycle) {
return amount;
}
double factor = Math.max(0, (issuanceHeight - chainHeightOfPastCycle) / (double) (currentBlockHeight - chainHeightOfPastCycle));
long weighted = Math.round(amount * factor);
return Math.max(0, weighted);
}
private void addBurnOutputModel(int chainHeight,
Map<P2PDataStorage.ByteArray, Set<TxOutput>> proofOfBurnOpReturnTxOutputByHash,
String name,
BurningManCandidate candidate) {
getProofOfBurnOpReturnTxOutputSetForName(proofOfBurnOpReturnTxOutputByHash, name)
.forEach(burnOutput -> {
int burnOutputHeight = burnOutput.getBlockHeight();
Optional<Tx> optionalTx = daoStateService.getTx(burnOutput.getTxId());
long burnOutputAmount = optionalTx.map(Tx::getBurntBsq).orElse(0L);
long decayedBurnOutputAmount = getDecayedBurnedAmount(burnOutputAmount, burnOutputHeight, chainHeight);
long date = optionalTx.map(BaseTx::getTime).orElse(0L);
int cycleIndex = cyclesInDaoStateService.getCycleIndexAtChainHeight(burnOutputHeight);
candidate.addBurnOutputModel(new BurnOutputModel(burnOutputAmount,
decayedBurnOutputAmount,
burnOutputHeight,
burnOutput.getTxId(),
date,
cycleIndex));
});
}
private static Set<TxOutput> getProofOfBurnOpReturnTxOutputSetForName(Map<P2PDataStorage.ByteArray, Set<TxOutput>> proofOfBurnOpReturnTxOutputByHash,
String name) {
byte[] preImage = name.getBytes(Charsets.UTF_8);
byte[] hash = ProofOfBurnConsensus.getHash(preImage);
P2PDataStorage.ByteArray key = new P2PDataStorage.ByteArray(hash);
if (proofOfBurnOpReturnTxOutputByHash.containsKey(key)) {
return proofOfBurnOpReturnTxOutputByHash.get(key);
} else {
return new HashSet<>();
}
}
private long getDecayedBurnedAmount(long amount, int issuanceHeight, int chainHeight) {
int chainHeightOfPastCycle = cyclesInDaoStateService.getChainHeightOfPastCycle(chainHeight, NUM_CYCLES_BURN_AMOUNT_DECAY);
return getDecayedAmount(amount,
issuanceHeight,
chainHeight,
chainHeightOfPastCycle);
}
private long getDecayedGenesisOutputAmount(long amount) {
return Math.round(amount * GENESIS_OUTPUT_AMOUNT_FACTOR);
}
}

View file

@ -0,0 +1,188 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman;
import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.dao.burningman.model.BurningManCandidate;
import bisq.core.dao.state.DaoStateListener;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.model.blockchain.Block;
import bisq.common.util.Tuple2;
import javax.inject.Inject;
import javax.inject.Singleton;
import com.google.common.annotations.VisibleForTesting;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
/**
* Used in the trade protocol for creating and verifying the delayed payout transaction.
* Requires to be deterministic.
* Changes in the parameters related to the receivers list could break verification of the peers
* delayed payout transaction in case not both are using the same version.
*/
@Slf4j
@Singleton
public class DelayedPayoutTxReceiverService implements DaoStateListener {
// 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.
private static final long DPT_MIN_OUTPUT_AMOUNT = 1000;
// 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
// gets still payouts.
private static final long DPT_MIN_REMAINDER_TO_LEGACY_BM = 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
// 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;
@Inject
public DelayedPayoutTxReceiverService(DaoStateService daoStateService, BurningManService burningManService) {
this.daoStateService = daoStateService;
this.burningManService = burningManService;
daoStateService.addDaoStateListener(this);
daoStateService.getLastBlock().ifPresent(this::applyBlock);
}
///////////////////////////////////////////////////////////////////////////////////////////
// DaoStateListener
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onParseBlockCompleteAfterBatchProcessing(Block block) {
applyBlock(block);
}
private void applyBlock(Block block) {
currentChainHeight = block.getHeight();
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
// We use a snapshot blockHeight to avoid failed trades in case maker and taker have different block heights.
// The selection is deterministic based on DAO data.
// The block height is the last mod(10) height from the range of the last 10-20 blocks (139 -> 120; 140 -> 130, 141 -> 130).
// We do not have the latest dao state by that but can ensure maker and taker have the same block.
public int getBurningManSelectionHeight() {
return getSnapshotHeight(daoStateService.getGenesisBlockHeight(), currentChainHeight, 10);
}
public List<Tuple2<Long, String>> getReceivers(int burningManSelectionHeight,
long inputAmount,
long tradeTxFee) {
Collection<BurningManCandidate> 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
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.
// In case of very large taker fee tx we would get a too high fee, but as fee rate is anyway rather
// arbitrary and volatile we are on the safer side. The delayed payout tx is published long after the
// take offer event and the recommended fee at that moment might be very different to actual
// recommended fee. To avoid that the delayed payout tx would get stuck due too low fees we use a
// min. fee rate of 10 sat/vByte.
// Deposit tx has a clearly defined structure, so we know the size. It is only one optional output if range amount offer was taken.
// 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));
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);
// 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()
.filter(candidate -> candidate.getMostRecentAddress().isPresent())
.mapToDouble(candidate -> {
double cappedBurnAmountShare = candidate.getCappedBurnAmountShare();
long amount = Math.round(cappedBurnAmountShare * spendableAmount);
return amount < minOutputAmount ? cappedBurnAmountShare : 0d;
})
.sum();
List<Tuple2<Long, String>> receivers = burningManCandidates.stream()
.filter(candidate -> candidate.getMostRecentAddress().isPresent())
.map(candidate -> {
double cappedBurnAmountShare = candidate.getCappedBurnAmountShare() / adjustment;
return new Tuple2<>(Math.round(cappedBurnAmountShare * spendableAmount),
candidate.getMostRecentAddress().get());
})
.filter(tuple -> tuple.first >= minOutputAmount)
.sorted(Comparator.<Tuple2<Long, String>, Long>comparing(tuple -> tuple.first)
.thenComparing(tuple -> tuple.second))
.collect(Collectors.toList());
long totalOutputValue = receivers.stream().mapToLong(e -> e.first).sum();
if (totalOutputValue < spendableAmount) {
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) {
receivers.add(new Tuple2<>(available, burningManService.getLegacyBurningManAddress(burningManSelectionHeight)));
}
}
return receivers;
}
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
// 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;
}
// 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) {
int ratio = (int) Math.round(height / (double) grid);
return ratio * grid - grid;
} else {
return genesisHeight;
}
}
}

View file

@ -0,0 +1,257 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting;
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.ReceivedBtcBalanceEntry;
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
import bisq.core.dao.burningman.accounting.blockchain.AccountingTx;
import bisq.core.dao.burningman.accounting.exceptions.BlockHashNotConnectingException;
import bisq.core.dao.burningman.accounting.exceptions.BlockHeightNotConnectingException;
import bisq.core.dao.burningman.accounting.storage.BurningManAccountingStoreService;
import bisq.core.monetary.Price;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences;
import bisq.core.util.AveragePriceUtil;
import bisq.common.UserThread;
import bisq.common.config.Config;
import bisq.common.util.DateUtil;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* Provides APIs for the accounting related aspects of burningmen.
* Combines the received funds from BTC trade fees and DPT payouts and the burned BSQ.
*/
@Slf4j
@Singleton
public class BurningManAccountingService implements DaoSetupService {
// now 763195 -> 107159 blocks takes about 14h
// First tx at BM address 656036 Sun Nov 08 19:02:18 EST 2020
// 2 months ago 754555
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;
private final BurningManAccountingStoreService burningManAccountingStoreService;
private final BurningManPresentationService burningManPresentationService;
private final TradeStatisticsManager tradeStatisticsManager;
private final Preferences preferences;
@Getter
private final Map<Date, Price> averageBsqPriceByMonth = new HashMap<>(getHistoricalAverageBsqPriceByMonth());
@Getter
private final Map<String, BalanceModel> balanceModelByBurningManName = new HashMap<>();
@Inject
public BurningManAccountingService(BurningManAccountingStoreService burningManAccountingStoreService,
BurningManPresentationService burningManPresentationService,
TradeStatisticsManager tradeStatisticsManager,
Preferences preferences) {
this.burningManAccountingStoreService = burningManAccountingStoreService;
this.burningManPresentationService = burningManPresentationService;
this.tradeStatisticsManager = tradeStatisticsManager;
this.preferences = preferences;
}
///////////////////////////////////////////////////////////////////////////////////////////
// DaoSetupService
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void addListeners() {
}
@Override
public void start() {
// Create the map from now back to the last entry of the historical data (April 2019-Nov. 2022).
averageBsqPriceByMonth.putAll(getAverageBsqPriceByMonth(new Date(), 2022, 10));
updateBalanceModelByAddress();
CompletableFuture.runAsync(() -> {
Map<String, BalanceModel> map = new HashMap<>();
// addAccountingBlockToBalanceModel takes about 500ms for 100k items, so we run it in a non UI thread.
getBlocks().forEach(block -> addAccountingBlockToBalanceModel(map, block));
UserThread.execute(() -> balanceModelByBurningManName.putAll(map));
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void onInitialBlockRequestsComplete() {
updateBalanceModelByAddress();
getBlocks().forEach(this::addAccountingBlockToBalanceModel);
}
public void onNewBlockReceived(AccountingBlock accountingBlock) {
updateBalanceModelByAddress();
addAccountingBlockToBalanceModel(accountingBlock);
}
public void addBlock(AccountingBlock block) throws BlockHashNotConnectingException, BlockHeightNotConnectingException {
if (!getBlocks().contains(block)) {
Optional<AccountingBlock> optionalLastBlock = getLastBlock();
if (optionalLastBlock.isPresent()) {
AccountingBlock lastBlock = optionalLastBlock.get();
if (block.getHeight() != lastBlock.getHeight() + 1) {
throw new BlockHeightNotConnectingException();
}
if (!Arrays.equals(block.getTruncatedPreviousBlockHash(), lastBlock.getTruncatedHash())) {
throw new BlockHashNotConnectingException();
}
} else if (block.getHeight() != EARLIEST_BLOCK_HEIGHT) {
throw new BlockHeightNotConnectingException();
}
log.info("Add new accountingBlock at height {} at {} with {} txs", block.getHeight(),
new Date(block.getDate()), block.getTxs().size());
burningManAccountingStoreService.addBlock(block);
} else {
log.info("We have that block already. Height: {}", block.getHeight());
}
}
public int getBlockHeightOfLastBlock() {
return getLastBlock().map(AccountingBlock::getHeight).orElse(BurningManAccountingService.EARLIEST_BLOCK_HEIGHT - 1);
}
public Optional<AccountingBlock> getLastBlock() {
return getBlocks().stream().max(Comparator.comparing(AccountingBlock::getHeight));
}
public Optional<AccountingBlock> getBlockAtHeight(int height) {
return getBlocks().stream().filter(block -> block.getHeight() == height).findAny();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Delegates
///////////////////////////////////////////////////////////////////////////////////////////
public List<AccountingBlock> getBlocks() {
return burningManAccountingStoreService.getBlocks();
}
public Map<String, String> getBurningManNameByAddress() {
return burningManPresentationService.getBurningManNameByAddress();
}
public String getGenesisTxId() {
return burningManPresentationService.getGenesisTxId();
}
public void purgeLastTenBlocks() {
burningManAccountingStoreService.purgeLastTenBlocks();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void updateBalanceModelByAddress() {
burningManPresentationService.getBurningManCandidatesByName().keySet()
.forEach(key -> balanceModelByBurningManName.putIfAbsent(key, new BalanceModel()));
}
private void addAccountingBlockToBalanceModel(AccountingBlock accountingBlock) {
addAccountingBlockToBalanceModel(balanceModelByBurningManName, accountingBlock);
}
private void addAccountingBlockToBalanceModel(Map<String, BalanceModel> balanceModelByBurningManName,
AccountingBlock accountingBlock) {
accountingBlock.getTxs().forEach(tx -> {
tx.getOutputs().forEach(txOutput -> {
String name = txOutput.getName();
balanceModelByBurningManName.putIfAbsent(name, new BalanceModel());
balanceModelByBurningManName.get(name).addReceivedBtcBalanceEntry(new ReceivedBtcBalanceEntry(tx.getTruncatedTxId(),
txOutput.getValue(),
new Date(accountingBlock.getDate()),
toBalanceEntryType(tx.getType())));
});
});
}
private Map<Date, Price> getAverageBsqPriceByMonth(Date from, int toYear, int toMonth) {
Map<Date, Price> averageBsqPriceByMonth = new HashMap<>();
Calendar calendar = new GregorianCalendar();
calendar.setTime(from);
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
do {
for (; month >= 0; month--) {
if (year == toYear && month == toMonth) {
break;
}
Date date = DateUtil.getStartOfMonth(year, month);
Price averageBsqPrice = AveragePriceUtil.getAveragePriceTuple(preferences, tradeStatisticsManager, 30, date).second;
averageBsqPriceByMonth.put(date, averageBsqPrice);
}
year--;
month = 11;
} while (year >= toYear);
return averageBsqPriceByMonth;
}
private static BalanceEntry.Type toBalanceEntryType(AccountingTx.Type type) {
return type == AccountingTx.Type.BTC_TRADE_FEE_TX ?
BalanceEntry.Type.BTC_TRADE_FEE_TX :
BalanceEntry.Type.DPT_TX;
}
@SuppressWarnings("CommentedOutCode")
private static Map<Date, Price> getHistoricalAverageBsqPriceByMonth() {
// We use the average 30 day BSQ price from the first day of a month back 30 days. So for 1.Nov 2022 we take the average during October 2022.
// Filling the map takes a bit of computation time (about 5 sec), so we use for historical data a pre-calculated list.
// Average price from 1. May 2019 (April average) - 1. Nov 2022 (Oct average)
String historical = "1648789200000=2735, 1630472400000=3376, 1612155600000=6235, 1559365200000=13139, 1659330000000=3609, 1633064400000=3196, 1583038800000=7578, 1622523600000=3918, 1625115600000=3791, 1667278800000=3794, 1561957200000=10882, 1593579600000=6153, 1577854800000=9034, 1596258000000=6514, 1604206800000=5642, 1643691600000=3021, 1606798800000=4946, 1569906000000=10445, 1567314000000=9885, 1614574800000=5052, 1656651600000=3311, 1638334800000=3015, 1564635600000=8788, 1635742800000=3065, 1654059600000=3207, 1646110800000=2824, 1609477200000=4199, 1664600400000=3820, 1662008400000=3756, 1556686800000=24094, 1588309200000=7986, 1585717200000=7994, 1627794000000=3465, 1580533200000=5094, 1590987600000=7411, 1619845200000=3956, 1617253200000=4024, 1575176400000=9571, 1572584400000=9058, 1641013200000=3052, 1601528400000=5648, 1651381200000=2908, 1598936400000=6032";
// Create historical data as string
/* log.info("averageBsqPriceByMonth=" + getAverageBsqPriceByMonth(new Date(), 2019, 3).entrySet().stream()
.map(e -> e.getKey().getTime() + "=" + e.getValue().getValue())
.collect(Collectors.toList()));
*/
return Arrays.stream(historical.split(", "))
.map(chunk -> chunk.split("="))
.collect(Collectors.toMap(tuple -> new Date(Long.parseLong(tuple[0])),
tuple -> Price.valueOf("BSQ", Long.parseLong(tuple[1]))));
}
}

View file

@ -0,0 +1,32 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.balance;
import java.util.Date;
public interface BalanceEntry {
Date getDate();
Date getMonth();
enum Type {
BTC_TRADE_FEE_TX,
DPT_TX,
BURN_TX
}
}

View file

@ -0,0 +1,124 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.balance;
import bisq.core.dao.burningman.model.BurnOutputModel;
import bisq.core.dao.burningman.model.BurningManCandidate;
import bisq.common.util.DateUtil;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import static bisq.core.dao.burningman.accounting.BurningManAccountingService.EARLIEST_DATE_MONTH;
import static bisq.core.dao.burningman.accounting.BurningManAccountingService.EARLIEST_DATE_YEAR;
@Slf4j
@EqualsAndHashCode
public class BalanceModel {
private final Set<ReceivedBtcBalanceEntry> receivedBtcBalanceEntries = new HashSet<>();
private final Map<Date, Set<ReceivedBtcBalanceEntry>> receivedBtcBalanceEntriesByMonth = new HashMap<>();
public BalanceModel() {
}
public void addReceivedBtcBalanceEntry(ReceivedBtcBalanceEntry balanceEntry) {
receivedBtcBalanceEntries.add(balanceEntry);
Date month = balanceEntry.getMonth();
receivedBtcBalanceEntriesByMonth.putIfAbsent(month, new HashSet<>());
receivedBtcBalanceEntriesByMonth.get(month).add(balanceEntry);
}
public Set<ReceivedBtcBalanceEntry> getReceivedBtcBalanceEntries() {
return receivedBtcBalanceEntries;
}
public Stream<BurnedBsqBalanceEntry> getBurnedBsqBalanceEntries(Set<BurnOutputModel> burnOutputModels) {
return burnOutputModels.stream()
.map(burnOutputModel -> new BurnedBsqBalanceEntry(burnOutputModel.getTxId(),
burnOutputModel.getAmount(),
new Date(burnOutputModel.getDate())));
}
public List<MonthlyBalanceEntry> getMonthlyBalanceEntries(BurningManCandidate burningManCandidate,
Predicate<BaseBalanceEntry> predicate) {
Map<Date, Set<BurnOutputModel>> burnOutputModelsByMonth = burningManCandidate.getBurnOutputModelsByMonth();
Set<Date> months = getMonths(new Date(), EARLIEST_DATE_YEAR, EARLIEST_DATE_MONTH);
return months.stream()
.map(date -> {
long sumBurnedBsq = 0;
Set<BalanceEntry.Type> types = new HashSet<>();
if (burnOutputModelsByMonth.containsKey(date)) {
Set<BurnOutputModel> burnOutputModels = burnOutputModelsByMonth.get(date);
Set<MonthlyBurnedBsqBalanceEntry> monthlyBurnedBsqBalanceEntries = burnOutputModels.stream()
.map(burnOutputModel -> new MonthlyBurnedBsqBalanceEntry(burnOutputModel.getTxId(),
burnOutputModel.getAmount(),
date))
.collect(Collectors.toSet());
sumBurnedBsq = monthlyBurnedBsqBalanceEntries.stream()
.filter(predicate)
.peek(e -> types.add(e.getType()))
.mapToLong(MonthlyBurnedBsqBalanceEntry::getAmount)
.sum();
}
long sumReceivedBtc = 0;
if (receivedBtcBalanceEntriesByMonth.containsKey(date)) {
sumReceivedBtc = receivedBtcBalanceEntriesByMonth.get(date).stream()
.filter(predicate)
.peek(e -> types.add(e.getType()))
.mapToLong(BaseBalanceEntry::getAmount)
.sum();
}
return new MonthlyBalanceEntry(sumReceivedBtc, sumBurnedBsq, date, types);
})
.filter(balanceEntry -> balanceEntry.getBurnedBsq() > 0 || balanceEntry.getReceivedBtc() > 0)
.collect(Collectors.toList());
}
private Set<Date> getMonths(Date from, int toYear, int toMonth) {
Set<Date> map = new HashSet<>();
Calendar calendar = new GregorianCalendar();
calendar.setTime(from);
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
do {
for (; month >= 0; month--) {
if (year == toYear && month == toMonth) {
break;
}
map.add(DateUtil.getStartOfMonth(year, month));
}
year--;
month = 11;
} while (year >= toYear);
return map;
}
}

View file

@ -0,0 +1,56 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.balance;
import bisq.common.util.DateUtil;
import java.util.Date;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@EqualsAndHashCode
@Getter
public abstract class BaseBalanceEntry implements BalanceEntry {
private final String txId;
private final long amount;
private final Date date;
private final Date month;
private final Type type;
protected BaseBalanceEntry(String txId, long amount, Date date, Type type) {
this.txId = txId;
this.amount = amount;
this.date = date;
month = DateUtil.getStartOfMonth(date);
this.type = type;
}
@Override
public String toString() {
return "BaseBalanceEntry{" +
"\r\n txId=" + txId +
"\r\n amount=" + amount +
",\r\n date=" + date +
",\r\n month=" + month +
",\r\n type=" + type +
"\r\n}";
}
}

View file

@ -0,0 +1,31 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.balance;
import java.util.Date;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode(callSuper = true)
@Getter
public class BurnedBsqBalanceEntry extends BaseBalanceEntry {
public BurnedBsqBalanceEntry(String txId, long amount, Date date) {
super(txId, amount, date, Type.BURN_TX);
}
}

View file

@ -0,0 +1,44 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.balance;
import bisq.common.util.DateUtil;
import java.util.Date;
import java.util.Set;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode
@Getter
public class MonthlyBalanceEntry implements BalanceEntry {
private final long receivedBtc;
private final long burnedBsq;
private final Date date;
private final Date month;
private final Set<Type> types;
public MonthlyBalanceEntry(long receivedBtc, long burnedBsq, Date date, Set<Type> types) {
this.receivedBtc = receivedBtc;
this.burnedBsq = burnedBsq;
this.date = date;
month = DateUtil.getStartOfMonth(date);
this.types = types;
}
}

View file

@ -0,0 +1,31 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.balance;
import java.util.Date;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode(callSuper = true)
@Getter
public class MonthlyBurnedBsqBalanceEntry extends BurnedBsqBalanceEntry {
public MonthlyBurnedBsqBalanceEntry(String txId, long amount, Date date) {
super(txId, amount, date);
}
}

View file

@ -0,0 +1,34 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.balance;
import bisq.common.util.Hex;
import java.util.Date;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode(callSuper = true)
@Getter
public class ReceivedBtcBalanceEntry extends BaseBalanceEntry {
// We store only last 4 bytes in the AccountTx which is used to create a ReceivedBtcBalanceEntry instance.
public ReceivedBtcBalanceEntry(byte[] truncatedTxId, long amount, Date date, BalanceEntry.Type type) {
super(Hex.encode(truncatedTxId), amount, date, type);
}
}

View file

@ -0,0 +1,112 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.blockchain;
import bisq.common.proto.network.NetworkPayload;
import bisq.common.util.Hex;
import com.google.protobuf.ByteString;
import java.util.List;
import java.util.stream.Collectors;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
// Block data is aggressively optimized for minimal size.
// Block has 21 bytes base cost
// Tx has 2 byte base cost.
// TxOutput has variable byte size depending on name length, usually about 10-20 bytes.
// Some extra overhead of a few bytes is present depending on lists filled or not.
// Example fee tx (1 output) has about 40 bytes
// Example DPT tx with 2 outputs has about 60 bytes, typical DPT with 15-20 outputs might have 500 bytes.
// 2 year legacy BM had about 100k fee txs and 1000 DPTs. Would be about 4MB for fee txs and 500kB for DPT.
// As most blocks have at least 1 tx we might not have empty blocks.
// With above estimates we can expect about 2 MB growth per year.
@Slf4j
@EqualsAndHashCode
public final class AccountingBlock implements NetworkPayload {
@Getter
private final int height;
private final int timeInSec;
// We use only last 4 bytes of 32 byte hash to save space.
// We use a byte array for flexibility if we would need to change the length of the hash later.
@Getter
private final byte[] truncatedHash;
@Getter
private final byte[] truncatedPreviousBlockHash;
@Getter
private final List<AccountingTx> txs;
public AccountingBlock(int height,
int timeInSec,
byte[] truncatedHash,
byte[] truncatedPreviousBlockHash,
List<AccountingTx> txs) {
this.height = height;
this.timeInSec = timeInSec;
this.truncatedHash = truncatedHash;
this.truncatedPreviousBlockHash = truncatedPreviousBlockHash;
this.txs = txs;
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public protobuf.AccountingBlock toProtoMessage() {
return protobuf.AccountingBlock.newBuilder()
.setHeight(height)
.setTimeInSec(timeInSec)
.setTruncatedHash(ByteString.copyFrom(truncatedHash))
.setTruncatedPreviousBlockHash(ByteString.copyFrom(truncatedPreviousBlockHash))
.addAllTxs(txs.stream().map(AccountingTx::toProtoMessage).collect(Collectors.toList()))
.build();
}
public static AccountingBlock fromProto(protobuf.AccountingBlock proto) {
List<AccountingTx> txs = proto.getTxsList().stream()
.map(AccountingTx::fromProto)
.collect(Collectors.toList());
// log.error("AccountingBlock.getSerializedSize {}, txs.size={}", proto.getSerializedSize(), txs.size());
return new AccountingBlock(proto.getHeight(),
proto.getTimeInSec(),
proto.getTruncatedHash().toByteArray(),
proto.getTruncatedPreviousBlockHash().toByteArray(),
txs);
}
public long getDate() {
return timeInSec * 1000L;
}
@Override
public String toString() {
return "AccountingBlock{" +
"\r\n height=" + height +
",\r\n timeInSec=" + timeInSec +
",\r\n truncatedHash=" + Hex.encode(truncatedHash) +
",\r\n truncatedPreviousBlockHash=" + Hex.encode(truncatedPreviousBlockHash) +
",\r\n txs=" + txs +
"\r\n}";
}
}

View file

@ -0,0 +1,87 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.blockchain;
import bisq.common.proto.network.NetworkPayload;
import bisq.common.util.Hex;
import com.google.protobuf.ByteString;
import java.util.List;
import java.util.stream.Collectors;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@EqualsAndHashCode
@Getter
public final class AccountingTx implements NetworkPayload {
public enum Type {
BTC_TRADE_FEE_TX,
DPT_TX
}
private final Type type;
private final List<AccountingTxOutput> outputs;
// We store only last 4 bytes to have a unique ID. Chance for collusion is very low, and we take that risk that
// one object might get overridden in a hashset by the colluding truncatedTxId and all other data being the same as well.
private final byte[] truncatedTxId;
public AccountingTx(Type type, List<AccountingTxOutput> outputs, String txId) {
this(type, outputs, Hex.decodeLast4Bytes(txId));
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private AccountingTx(Type type, List<AccountingTxOutput> outputs, byte[] truncatedTxId) {
this.type = type;
this.outputs = outputs;
this.truncatedTxId = truncatedTxId;
}
@Override
public protobuf.AccountingTx toProtoMessage() {
return protobuf.AccountingTx.newBuilder()
.setType(type.ordinal())
.addAllOutputs(outputs.stream().map(AccountingTxOutput::toProtoMessage).collect(Collectors.toList()))
.setTruncatedTxId(ByteString.copyFrom(truncatedTxId)).build();
}
public static AccountingTx fromProto(protobuf.AccountingTx proto) {
List<AccountingTxOutput> outputs = proto.getOutputsList().stream()
.map(AccountingTxOutput::fromProto)
.collect(Collectors.toList());
return new AccountingTx(Type.values()[proto.getType()],
outputs,
proto.getTruncatedTxId().toByteArray());
}
@Override
public String toString() {
return "AccountingTx{" +
",\n type='" + type + '\'' +
",\n outputs=" + outputs +
",\n truncatedTxId=" + Hex.encode(truncatedTxId) +
"\n }";
}
}

View file

@ -0,0 +1,93 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.blockchain;
import bisq.core.dao.burningman.BurningManPresentationService;
import bisq.common.proto.network.NetworkPayload;
import org.bitcoinj.core.Coin;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkArgument;
// Outputs get pruned to required number of outputs depending on tx type.
// We store value as integer in protobuf as we do not support larger amounts than 21.47483647 BTC for our tx types.
// Name is burningman candidate name. For legacy Burningman we shorten to safe space.
@Slf4j
@EqualsAndHashCode
public final class AccountingTxOutput implements NetworkPayload {
private static final String LEGACY_BM_FEES_SHORT = "LBMF";
private static final String LEGACY_BM_DPT_SHORT = "LBMD";
@Getter
private final long value;
private final String name;
public AccountingTxOutput(long value, String name) {
this.value = value;
this.name = maybeShortenLBM(name);
}
private String maybeShortenLBM(String name) {
return name.equals(BurningManPresentationService.LEGACY_BURNING_MAN_BTC_FEES_NAME) ?
LEGACY_BM_FEES_SHORT :
name.equals(BurningManPresentationService.LEGACY_BURNING_MAN_DPT_NAME) ?
LEGACY_BM_DPT_SHORT :
name;
}
public String getName() {
return name.equals(LEGACY_BM_FEES_SHORT) ?
BurningManPresentationService.LEGACY_BURNING_MAN_BTC_FEES_NAME :
name.equals(LEGACY_BM_DPT_SHORT) ?
BurningManPresentationService.LEGACY_BURNING_MAN_DPT_NAME :
name;
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public protobuf.AccountingTxOutput toProtoMessage() {
checkArgument(value < Coin.valueOf(2147483647).getValue(),
"We only support integer value in protobuf storage for the amount and it need to be below 21.47483647 BTC");
return protobuf.AccountingTxOutput.newBuilder()
.setValue((int) value)
.setName(name).build();
}
public static AccountingTxOutput fromProto(protobuf.AccountingTxOutput proto) {
int intValue = proto.getValue();
checkArgument(intValue >= 0, "Value must not be negative");
return new AccountingTxOutput(intValue, proto.getName());
}
@Override
public String toString() {
return "AccountingTxOutput{" +
",\r\n value=" + value +
",\r\n name='" + name + '\'' +
"\r\n}";
}
}

View file

@ -0,0 +1,75 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.blockchain.temp;
import bisq.core.dao.node.full.rpc.dto.DtoPubKeyScript;
import bisq.core.dao.node.full.rpc.dto.RawDtoTransaction;
import bisq.core.dao.state.model.blockchain.ScriptType;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@EqualsAndHashCode
@Getter
public final class TempAccountingTx {
private final String txId;
private final boolean isValidDptLockTime;
private final List<TempAccountingTxInput> inputs;
private final List<TempAccountingTxOutput> outputs;
public TempAccountingTx(RawDtoTransaction tx) {
txId = tx.getTxId();
// If lockTime is < 500000000 it is interpreted as block height, otherwise as unix time. We use block height.
// We only handle blocks from EARLIEST_BLOCK_HEIGHT on
//todo for dev testing
isValidDptLockTime = /*tx.getLockTime() >= BurningManAccountingService.EARLIEST_BLOCK_HEIGHT &&*/ tx.getLockTime() < 500000000;
inputs = tx.getVIn().stream()
.map(input -> {
List<String> txInWitness = input.getTxInWitness() != null ? input.getTxInWitness() : new ArrayList<>();
return new TempAccountingTxInput(input.getSequence(), txInWitness);
})
.collect(Collectors.toList());
outputs = tx.getVOut().stream()
.map(output -> {
long value = BigDecimal.valueOf(output.getValue()).movePointRight(8).longValueExact();
// We use a non-null field for address as in the final object we require that the address is available
String address = "";
DtoPubKeyScript scriptPubKey = output.getScriptPubKey();
if (scriptPubKey != null) {
List<String> addresses = scriptPubKey.getAddresses();
if (addresses != null && addresses.size() == 1) {
address = addresses.get(0);
}
}
ScriptType scriptType = output.getScriptPubKey() != null ? output.getScriptPubKey().getType() : null;
return new TempAccountingTxOutput(value, address, scriptType);
})
.collect(Collectors.toList());
}
}

View file

@ -0,0 +1,37 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.blockchain.temp;
import java.util.List;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@EqualsAndHashCode
@Getter
public final class TempAccountingTxInput {
private final long sequence;
private final List<String> txInWitness;
public TempAccountingTxInput(long sequence, List<String> txInWitness) {
this.sequence = sequence;
this.txInWitness = txInWitness;
}
}

View file

@ -0,0 +1,37 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.blockchain.temp;
import bisq.core.dao.state.model.blockchain.ScriptType;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode
@Getter
public final class TempAccountingTxOutput {
private final long value;
private final String address;
private final ScriptType scriptType;
public TempAccountingTxOutput(long value, String address, ScriptType scriptType) {
this.value = value;
this.address = address;
this.scriptType = scriptType;
}
}

View file

@ -0,0 +1,26 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.exceptions;
import lombok.Getter;
@Getter
public class BlockHashNotConnectingException extends Exception {
public BlockHashNotConnectingException() {
}
}

View file

@ -0,0 +1,26 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.exceptions;
import lombok.Getter;
@Getter
public class BlockHeightNotConnectingException extends Exception {
public BlockHeightNotConnectingException() {
}
}

View file

@ -0,0 +1,237 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.node;
import bisq.core.dao.DaoSetupService;
import bisq.core.dao.burningman.accounting.BurningManAccountingService;
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
import bisq.core.dao.burningman.accounting.node.full.AccountingBlockParser;
import bisq.core.dao.state.DaoStateListener;
import bisq.core.dao.state.DaoStateService;
import bisq.network.p2p.BootstrapListener;
import bisq.network.p2p.P2PService;
import bisq.common.UserThread;
import bisq.common.app.DevEnv;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Sha256Hash;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.Set;
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static org.bitcoinj.core.Utils.HEX;
@Slf4j
public abstract class AccountingNode implements DaoSetupService, DaoStateListener {
public static final Set<String> PERMITTED_PUB_KEYS = Set.of("034527b1c2b644283c19c180efbcc9ba51258fbe5c5ce0c95b522f1c9b07896e48",
"02a06121632518eef4400419a3aaf944534e8cf357138dd82bba3ad78ce5902f27",
"029ff0da89aa03507dfe0529eb74a53bc65fbee7663ceba04f014b0ee4520973b5",
"0205c992604969bc70914fc89a6daa43cd5157ac886224a8c0901dd1dc6dd1df45",
"023e699281b3ee41f35991f064a5c12cb1b61286dda22c8220b3f707aa21235efb");
///////////////////////////////////////////////////////////////////////////////////////////
// Oracle verification
///////////////////////////////////////////////////////////////////////////////////////////
public static Sha256Hash getSha256Hash(AccountingBlock block) {
return Sha256Hash.of(block.toProtoMessage().toByteArray());
}
@Nullable
public static Sha256Hash getSha256Hash(Collection<AccountingBlock> blocks) {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
for (AccountingBlock accountingBlock : blocks) {
outputStream.write(accountingBlock.toProtoMessage().toByteArray());
}
return Sha256Hash.of(outputStream.toByteArray());
} catch (IOException e) {
return null;
}
}
public static byte[] getSignature(Sha256Hash sha256Hash, ECKey privKey) {
ECKey.ECDSASignature ecdsaSignature = privKey.sign(sha256Hash);
return ecdsaSignature.encodeToDER();
}
public static boolean isValidPubKeyAndSignature(Sha256Hash sha256Hash,
String pubKey,
byte[] signature,
boolean useDevPrivilegeKeys) {
if (!getPermittedPubKeys(useDevPrivilegeKeys).contains(pubKey)) {
log.warn("PubKey is not in supported key set. pubKey={}", pubKey);
return false;
}
try {
ECKey.ECDSASignature ecdsaSignature = ECKey.ECDSASignature.decodeFromDER(signature);
ECKey ecPubKey = ECKey.fromPublicOnly(HEX.decode(pubKey));
return ecPubKey.verify(sha256Hash, ecdsaSignature);
} catch (Throwable e) {
log.warn("Signature verification failed.");
return false;
}
}
public static boolean isPermittedPubKey(boolean useDevPrivilegeKeys, String pubKey) {
return getPermittedPubKeys(useDevPrivilegeKeys).contains(pubKey);
}
private static Set<String> getPermittedPubKeys(boolean useDevPrivilegeKeys) {
return useDevPrivilegeKeys ? Set.of(DevEnv.DEV_PRIVILEGE_PUB_KEY) : PERMITTED_PUB_KEYS;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Class fields
///////////////////////////////////////////////////////////////////////////////////////////
protected final P2PService p2PService;
private final DaoStateService daoStateService;
protected final BurningManAccountingService burningManAccountingService;
protected final AccountingBlockParser accountingBlockParser;
@Nullable
protected Consumer<String> errorMessageHandler;
@Nullable
protected Consumer<String> warnMessageHandler;
protected BootstrapListener bootstrapListener;
protected int tryReorgCounter;
protected boolean p2pNetworkReady;
protected boolean initialBlockRequestsComplete;
public AccountingNode(P2PService p2PService,
DaoStateService daoStateService,
BurningManAccountingService burningManAccountingService,
AccountingBlockParser accountingBlockParser) {
this.p2PService = p2PService;
this.daoStateService = daoStateService;
this.burningManAccountingService = burningManAccountingService;
this.accountingBlockParser = accountingBlockParser;
}
///////////////////////////////////////////////////////////////////////////////////////////
// DaoStateListener
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onParseBlockChainComplete() {
onInitialDaoBlockParsingComplete();
// We get called onParseBlockChainComplete at each new block arriving but we want to react only after initial
// parsing is done, so we remove after getting called ourself as listener.
daoStateService.removeDaoStateListener(this);
}
///////////////////////////////////////////////////////////////////////////////////////////
// DaoSetupService
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void addListeners() {
if (daoStateService.isParseBlockChainComplete()) {
log.info("daoStateService.isParseBlockChainComplete is already true, " +
"we call onInitialDaoBlockParsingComplete directly");
onInitialDaoBlockParsingComplete();
} else {
daoStateService.addDaoStateListener(this);
}
bootstrapListener = new BootstrapListener() {
@Override
public void onNoSeedNodeAvailable() {
onP2PNetworkReady();
}
@Override
public void onUpdatedDataReceived() {
onP2PNetworkReady();
}
};
}
@Override
public abstract void start();
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void setErrorMessageHandler(@SuppressWarnings("NullableProblems") Consumer<String> errorMessageHandler) {
this.errorMessageHandler = errorMessageHandler;
}
public void setWarnMessageHandler(@SuppressWarnings("NullableProblems") Consumer<String> warnMessageHandler) {
this.warnMessageHandler = warnMessageHandler;
}
public abstract void shutDown();
///////////////////////////////////////////////////////////////////////////////////////////
// Protected
///////////////////////////////////////////////////////////////////////////////////////////
protected abstract void onInitialDaoBlockParsingComplete();
protected void onInitialized() {
if (p2PService.isBootstrapped()) {
log.info("p2PService.isBootstrapped is already true, we call onP2PNetworkReady directly.");
onP2PNetworkReady();
} else {
p2PService.addP2PServiceListener(bootstrapListener);
}
}
protected void onP2PNetworkReady() {
p2pNetworkReady = true;
p2PService.removeP2PServiceListener(bootstrapListener);
}
protected abstract void startRequestBlocks();
protected void onInitialBlockRequestsComplete() {
initialBlockRequestsComplete = true;
burningManAccountingService.onInitialBlockRequestsComplete();
}
protected void applyReOrg() {
log.warn("applyReOrg called");
tryReorgCounter++;
if (tryReorgCounter < 5) {
burningManAccountingService.purgeLastTenBlocks();
// Increase delay at each retry
UserThread.runAfter(this::startRequestBlocks, (long) tryReorgCounter * tryReorgCounter);
} else {
log.warn("We tried {} times to request blocks again after a reorg signal but it is still failing.", tryReorgCounter);
}
}
}

View file

@ -0,0 +1,57 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.node;
import bisq.core.dao.burningman.BurningManService;
import bisq.core.dao.burningman.accounting.node.full.AccountingFullNode;
import bisq.core.dao.burningman.accounting.node.lite.AccountingLiteNode;
import bisq.core.user.Preferences;
import bisq.common.config.Config;
import com.google.inject.Inject;
import javax.inject.Named;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class AccountingNodeProvider {
@Getter
private final AccountingNode accountingNode;
@Inject
public AccountingNodeProvider(AccountingLiteNode liteNode,
AccountingFullNode fullNode,
InActiveAccountingNode inActiveAccountingNode,
@Named(Config.IS_BM_FULL_NODE) boolean isBmFullNode,
Preferences preferences) {
boolean rpcDataSet = preferences.getRpcUser() != null &&
!preferences.getRpcUser().isEmpty()
&& preferences.getRpcPw() != null &&
!preferences.getRpcPw().isEmpty() &&
preferences.getBlockNotifyPort() > 0;
if (BurningManService.isActivated()) {
accountingNode = isBmFullNode && rpcDataSet ? fullNode : liteNode;
} else {
accountingNode = inActiveAccountingNode;
}
}
}

View file

@ -0,0 +1,60 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.node;
import bisq.core.dao.burningman.accounting.BurningManAccountingService;
import bisq.core.dao.burningman.accounting.node.full.AccountingBlockParser;
import bisq.core.dao.state.DaoStateService;
import bisq.network.p2p.P2PService;
import com.google.inject.Inject;
import javax.inject.Singleton;
// Dummy implementation for a do-nothing AccountingNode. Used for the time before the burningman domain gets activated.
@Singleton
class InActiveAccountingNode extends AccountingNode {
@Inject
public InActiveAccountingNode(P2PService p2PService,
DaoStateService daoStateService,
BurningManAccountingService burningManAccountingService,
AccountingBlockParser accountingBlockParser) {
super(p2PService, daoStateService, burningManAccountingService, accountingBlockParser);
}
@Override
public void addListeners() {
}
@Override
public void start() {
}
@Override
public void shutDown() {
}
@Override
protected void onInitialDaoBlockParsingComplete() {
}
@Override
protected void startRequestBlocks() {
}
}

View file

@ -0,0 +1,210 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.node.full;
import bisq.core.dao.burningman.accounting.BurningManAccountingService;
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
import bisq.core.dao.burningman.accounting.blockchain.AccountingTx;
import bisq.core.dao.burningman.accounting.blockchain.AccountingTxOutput;
import bisq.core.dao.burningman.accounting.blockchain.temp.TempAccountingTx;
import bisq.core.dao.burningman.accounting.blockchain.temp.TempAccountingTxInput;
import bisq.core.dao.burningman.accounting.blockchain.temp.TempAccountingTxOutput;
import bisq.core.dao.node.full.rpc.dto.RawDtoBlock;
import bisq.core.dao.state.model.blockchain.ScriptType;
import bisq.common.util.Hex;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.TransactionInput;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Singleton
public class AccountingBlockParser {
private final BurningManAccountingService burningManAccountingService;
@Inject
public AccountingBlockParser(BurningManAccountingService burningManAccountingService) {
this.burningManAccountingService = burningManAccountingService;
}
public AccountingBlock parse(RawDtoBlock rawDtoBlock) {
Map<String, String> burningManNameByAddress = burningManAccountingService.getBurningManNameByAddress();
String genesisTxId = burningManAccountingService.getGenesisTxId();
// We filter early for first output address match. DPT txs have multiple outputs which need to match and will be checked later.
Set<String> receiverAddresses = burningManNameByAddress.keySet();
List<AccountingTx> txs = rawDtoBlock.getTx().stream()
.map(TempAccountingTx::new)
.filter(tempAccountingTx -> receiverAddresses.contains(tempAccountingTx.getOutputs().get(0).getAddress()))
.map(tempAccountingTx -> toAccountingTx(tempAccountingTx, burningManNameByAddress, genesisTxId))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
// Time in rawDtoBlock is in seconds
int timeInSec = rawDtoBlock.getTime().intValue();
byte[] truncatedHash = Hex.decodeLast4Bytes(rawDtoBlock.getHash());
byte[] truncatedPreviousBlockHash = Hex.decodeLast4Bytes(rawDtoBlock.getPreviousBlockHash());
return new AccountingBlock(rawDtoBlock.getHeight(),
timeInSec,
truncatedHash,
truncatedPreviousBlockHash,
txs);
}
// We cannot know for sure if it's a DPT or BTC fee tx as we do not have the spending tx output with more
// data for verification as we do not keep the full blockchain data for lookup unspent tx outputs.
// The DPT can be very narrowly detected. The BTC fee txs might have false positives.
private Optional<AccountingTx> toAccountingTx(TempAccountingTx tempAccountingTx,
Map<String, String> burningManNameByAddress,
String genesisTxId) {
if (genesisTxId.equals(tempAccountingTx.getTxId())) {
return Optional.empty();
}
Set<String> receiverAddresses = burningManNameByAddress.keySet();
// DPT has 1 input from P2WSH with lock time and sequence number set.
// We only use native segwit P2SH, so we expect a txInWitness
List<TempAccountingTxInput> inputs = tempAccountingTx.getInputs();
List<TempAccountingTxOutput> outputs = tempAccountingTx.getOutputs();
// Max DPT output amount is currently 4 BTC. Max. security deposit is 50% of trade amount.
// We give some extra headroom to cover potential future changes.
// We store value as integer in protobuf to safe space, so max. possible value is 21.47483647 BTC.
long maxDptValue = 6 * Coin.COIN.getValue();
if (inputs.size() == 1 &&
isValidTimeLock(tempAccountingTx, inputs.get(0)) &&
doAllOutputsMatchReceivers(outputs, receiverAddresses) &&
isWitnessDataWP2SH(inputs.get(0).getTxInWitness()) &&
outputs.stream().allMatch(txOutput -> isExpectedScriptType(txOutput, tempAccountingTx)) &&
outputs.stream().allMatch(txOutput -> txOutput.getValue() <= maxDptValue)) {
List<AccountingTxOutput> accountingTxOutputs = outputs.stream()
.map(output -> new AccountingTxOutput(output.getValue(), burningManNameByAddress.get(output.getAddress())))
.collect(Collectors.toList());
return Optional.of(new AccountingTx(AccountingTx.Type.DPT_TX, accountingTxOutputs, tempAccountingTx.getTxId()));
}
// BTC trade fee tx has 2 or 3 outputs.
// First output to receiver, second reserved for trade and an optional 3rd for change.
// Address check is done in parse method above already.
// Amounts are in a certain range but as fee can change with DAO param changes we cannot set hard limits.
// The min. trade fee in Nov. 2022 is 5000 sat. We use 2500 as lower bound.
// The largest taker fee for a 2 BTC trade is about 0.0059976 BTC (599_760 sat). We use 1_000_000 sat as upper bound.
// Inputs are not constrained.
// Max fees amount is currently 0.0176 BTC.
// We give some extra headroom to cover potential future fee changes.
// We store value as integer in protobuf to safe space, so max. possible value is 21.47483647 BTC.
long maxTradeFeeValue = Coin.parseCoin("0.1").getValue();
TempAccountingTxOutput firstOutput = outputs.get(0);
if (outputs.size() >= 2 &&
outputs.size() <= 3 &&
firstOutput.getValue() > 2500 &&
firstOutput.getValue() < 1_000_000 &&
isExpectedScriptType(firstOutput, tempAccountingTx) &&
firstOutput.getValue() <= maxTradeFeeValue) {
// We only keep first output.
String name = burningManNameByAddress.get(firstOutput.getAddress());
return Optional.of(new AccountingTx(AccountingTx.Type.BTC_TRADE_FEE_TX,
List.of(new AccountingTxOutput(firstOutput.getValue(), name)),
tempAccountingTx.getTxId()));
}
return Optional.empty();
}
// TODO not sure if other ScriptType are to be expected
private boolean isExpectedScriptType(TempAccountingTxOutput txOutput, TempAccountingTx accountingTx) {
boolean result = txOutput.getScriptType() != null &&
(txOutput.getScriptType() == ScriptType.PUB_KEY_HASH ||
txOutput.getScriptType() == ScriptType.SCRIPT_HASH ||
txOutput.getScriptType() == ScriptType.WITNESS_V0_KEYHASH);
if (!result) {
log.error("isExpectedScriptType txOutput.getScriptType()={}, txIf={}", txOutput.getScriptType(), accountingTx.getTxId());
}
return result;
}
// All outputs need to be to receiver addresses (incl. legacy BM)
private boolean doAllOutputsMatchReceivers(List<TempAccountingTxOutput> outputs, Set<String> receiverAddresses) {
return outputs.stream().allMatch(output -> receiverAddresses.contains(output.getAddress()));
}
private boolean isValidTimeLock(TempAccountingTx accountingTx, TempAccountingTxInput firstInput) {
// Need to be 0xfffffffe
return accountingTx.isValidDptLockTime() && firstInput.getSequence() == TransactionInput.NO_SEQUENCE - 1;
}
/*
Example txInWitness: [, 304502210098fcec3ac1c1383d40159587b42e8b79cb3e793004d6ccb080bfb93f02c15f93022039f014eb933c59f988d68a61aa7a1c787f08d94bd0b222104718792798e43e3c01, 304402201f8b37f3b8b5b9944ca88f18f6bb888c5a48dc5183edf204ae6d3781032122e102204dc6397538055d94de1ab683315aac7d87289be0c014569f7b3fa465bf70b6d401, 5221027f6da96ede171617ce79ec305a76871ecce21ad737517b667fc9735f2dc342342102d8d93e02fb0833201274b47a302b47ff81c0b3510508eb0444cb1674d0d8a6d052ae]
*/
private boolean isWitnessDataWP2SH(List<String> txInWitness) {
// txInWitness from the 2of2 multiSig has 4 chunks.
// 0 byte, sig1, sig2, redeemScript
if (txInWitness.size() != 4) {
log.error("txInWitness chunks size not 4 .txInWitness={}", txInWitness);
return false;
}
// First chunk is o byte (empty string)
if (!txInWitness.get(0).isEmpty()) {
log.error("txInWitness.get(0) not empty .txInWitness={}", txInWitness);
return false;
}
// The 2 signatures are 70 - 73 bytes
int minSigLength = 140;
int maxSigLength = 146;
int fistSigLength = txInWitness.get(1).length();
if (fistSigLength < minSigLength || fistSigLength > maxSigLength) {
log.error("fistSigLength wrong .txInWitness={}", txInWitness);
return false;
}
int secondSigLength = txInWitness.get(2).length();
if (secondSigLength < minSigLength || secondSigLength > maxSigLength) {
log.error("secondSigLength wrong .txInWitness={}", txInWitness);
return false;
}
String redeemScript = txInWitness.get(3);
if (redeemScript.length() != 142) {
log.error("redeemScript not valid length .txInWitness={}", txInWitness);
return false;
}
// OP_2 pub1 pub2 OP_2 OP_CHECKMULTISIG
// In hex: "5221" + PUB_KEY_1 + "21" + PUB_KEY_2 + "52ae";
// PubKeys are 33 bytes -> length 66 in hex
String separator = redeemScript.substring(70, 72);
boolean result = redeemScript.startsWith("5221") &&
redeemScript.endsWith("52ae") &&
separator.equals("21");
if (!result) {
log.error("redeemScript not valid .txInWitness={}", txInWitness);
}
return result;
}
}

View file

@ -0,0 +1,323 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.node.full;
import bisq.core.dao.burningman.accounting.BurningManAccountingService;
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
import bisq.core.dao.burningman.accounting.exceptions.BlockHashNotConnectingException;
import bisq.core.dao.burningman.accounting.exceptions.BlockHeightNotConnectingException;
import bisq.core.dao.burningman.accounting.node.AccountingNode;
import bisq.core.dao.burningman.accounting.node.full.network.AccountingFullNodeNetworkService;
import bisq.core.dao.node.full.RpcException;
import bisq.core.dao.node.full.RpcService;
import bisq.core.dao.node.full.rpc.NotificationHandlerException;
import bisq.core.dao.node.full.rpc.dto.RawDtoBlock;
import bisq.core.dao.state.DaoStateService;
import bisq.network.p2p.P2PService;
import bisq.common.UserThread;
import bisq.common.handlers.ResultHandler;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.net.ConnectException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Singleton
public class AccountingFullNode extends AccountingNode {
private final RpcService rpcService;
private final AccountingFullNodeNetworkService accountingFullNodeNetworkService;
private boolean addBlockHandlerAdded;
private int batchedBlocks;
private long batchStartTime;
private final List<RawDtoBlock> pendingRawDtoBlocks = new ArrayList<>();
private int requestBlocksUpToHeadHeightCounter;
@Inject
public AccountingFullNode(P2PService p2PService,
DaoStateService daoStateService,
BurningManAccountingService burningManAccountingService,
AccountingBlockParser accountingBlockParser,
AccountingFullNodeNetworkService accountingFullNodeNetworkService,
RpcService rpcService) {
super(p2PService, daoStateService, burningManAccountingService, accountingBlockParser);
this.rpcService = rpcService;
this.accountingFullNodeNetworkService = accountingFullNodeNetworkService;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Public methods
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void start() {
// We do not start yes but wait until DAO block parsing is complete to not interfere with
// that higher priority activity.
}
@Override
public void shutDown() {
accountingFullNodeNetworkService.shutDown();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Protected
///////////////////////////////////////////////////////////////////////////////////////////
// We start after initial DAO parsing is complete
@Override
protected void onInitialDaoBlockParsingComplete() {
log.info("onInitialDaoBlockParsingComplete");
rpcService.setup(() -> {
accountingFullNodeNetworkService.addListeners();
super.onInitialized();
startRequestBlocks();
},
this::handleError);
}
@Override
protected void onP2PNetworkReady() {
super.onP2PNetworkReady();
if (initialBlockRequestsComplete) {
addBlockHandler();
int heightOfLastBlock = burningManAccountingService.getBlockHeightOfLastBlock();
log.info("onP2PNetworkReady: We run requestBlocksIfNewBlockAvailable with latest block height {}.", heightOfLastBlock);
requestBlocksIfNewBlockAvailable(heightOfLastBlock);
}
}
@Override
protected void startRequestBlocks() {
int heightOfLastBlock = burningManAccountingService.getBlockHeightOfLastBlock();
log.info("startRequestBlocks: heightOfLastBlock={}", heightOfLastBlock);
rpcService.requestChainHeadHeight(headHeight -> {
// If our persisted block is equal to the chain height we have heightOfLastBlock 1 block higher,
// so we do not call parseBlocksOnHeadHeight
log.info("rpcService.requestChainHeadHeight: headHeight={}", headHeight);
requestBlocksUpToHeadHeight(heightOfLastBlock, headHeight);
},
this::handleError);
}
@Override
protected void onInitialBlockRequestsComplete() {
super.onInitialBlockRequestsComplete();
if (p2pNetworkReady) {
addBlockHandler();
} else {
log.info("onParseBlockChainComplete but P2P network is not ready yet.");
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void addBlockHandler() {
if (!addBlockHandlerAdded) {
addBlockHandlerAdded = true;
rpcService.addNewRawDtoBlockHandler(rawDtoBlock -> {
parseBlock(rawDtoBlock).ifPresent(accountingBlock -> {
maybePublishAccountingBlock(accountingBlock);
burningManAccountingService.onNewBlockReceived(accountingBlock);
});
},
this::handleError);
}
}
private void maybePublishAccountingBlock(AccountingBlock accountingBlock) {
if (p2pNetworkReady && initialBlockRequestsComplete) {
accountingFullNodeNetworkService.publishAccountingBlock(accountingBlock);
}
}
private void requestBlocksIfNewBlockAvailable(int heightOfLastBlock) {
rpcService.requestChainHeadHeight(headHeight -> {
if (headHeight > heightOfLastBlock) {
log.info("During requests new blocks have arrived. We request again to get the missing blocks." +
"heightOfLastBlock={}, headHeight={}", heightOfLastBlock, headHeight);
requestBlocksUpToHeadHeight(heightOfLastBlock, headHeight);
} else {
log.info("requestChainHeadHeight did not result in a new block, so we complete.");
log.info("Requesting {} blocks took {} seconds", batchedBlocks, (System.currentTimeMillis() - batchStartTime) / 1000d);
if (!initialBlockRequestsComplete) {
onInitialBlockRequestsComplete();
}
}
},
this::handleError);
}
private void requestBlocksUpToHeadHeight(int heightOfLastBlock, int headHeight) {
if (heightOfLastBlock == headHeight) {
log.info("Out heightOfLastBlock is same as headHeight of Bitcoin Core.");
if (!initialBlockRequestsComplete) {
onInitialBlockRequestsComplete();
}
} else if (heightOfLastBlock < headHeight) {
batchedBlocks = headHeight - heightOfLastBlock;
batchStartTime = System.currentTimeMillis();
log.info("Request {} blocks from {} to {}", batchedBlocks, heightOfLastBlock, headHeight);
requestBlockRecursively(heightOfLastBlock,
headHeight,
() -> {
// We are done, but it might be that new blocks have arrived in the meantime,
// so we try again with heightOfLastBlock set to current headHeight
requestBlocksIfNewBlockAvailable(headHeight);
}, this::handleError);
} else {
requestBlocksUpToHeadHeightCounter++;
log.warn("We are trying to start with a block which is above the chain height of Bitcoin Core. " +
"We need probably wait longer until Bitcoin Core has fully synced. " +
"We try again after a delay of {} min.", requestBlocksUpToHeadHeightCounter * requestBlocksUpToHeadHeightCounter);
if (requestBlocksUpToHeadHeightCounter <= 5) {
UserThread.runAfter(() -> rpcService.requestChainHeadHeight(height ->
requestBlocksUpToHeadHeight(heightOfLastBlock, height),
this::handleError), requestBlocksUpToHeadHeightCounter * requestBlocksUpToHeadHeightCounter * 60L);
} else {
log.warn("We tried {} times to start with startBlockHeight {} which is above the chain height {} of Bitcoin Core. " +
"It might be that Bitcoin Core has not fully synced. We give up now.",
requestBlocksUpToHeadHeightCounter, heightOfLastBlock, headHeight);
}
}
}
private void requestBlockRecursively(int heightOfLastBlock,
int headHeight,
ResultHandler completeHandler,
Consumer<Throwable> errorHandler) {
int requestHeight = heightOfLastBlock + 1;
rpcService.requestRawDtoBlock(requestHeight,
rawDtoBlock -> {
parseBlock(rawDtoBlock).ifPresent(this::maybePublishAccountingBlock);
// Increment heightOfLastBlock and recursively call requestBlockRecursively until we reach headHeight
int newHeightOfLastBlock = burningManAccountingService.getBlockHeightOfLastBlock();
if (requestHeight < headHeight) {
requestBlockRecursively(newHeightOfLastBlock, headHeight, completeHandler, errorHandler);
} else {
// We are done
completeHandler.handleResult();
}
},
errorHandler);
}
private Optional<AccountingBlock> parseBlock(RawDtoBlock rawDtoBlock) {
// We check if we have a block with that height. If so we return. We do not use the chainHeight as with the earliest
// height we have no block but chainHeight is initially set to the earliest height (bad design ;-( but a bit tricky
// to change now as it used in many areas.)
if (burningManAccountingService.getBlockAtHeight(rawDtoBlock.getHeight()).isPresent()) {
log.info("We have already a block with the height of the new block. Height of new block={}", rawDtoBlock.getHeight());
return Optional.empty();
}
pendingRawDtoBlocks.remove(rawDtoBlock);
try {
AccountingBlock accountingBlock = accountingBlockParser.parse(rawDtoBlock);
burningManAccountingService.addBlock(accountingBlock);
// After parsing we check if we have pending future blocks.
// As we successfully added a new block a pending block might fit as next block.
if (!pendingRawDtoBlocks.isEmpty()) {
// We take only first element after sorting (so it is the accountingBlock with the next height) to avoid that
// we would repeat calls in recursions in case we would iterate the list.
pendingRawDtoBlocks.sort(Comparator.comparing(RawDtoBlock::getHeight));
RawDtoBlock nextPending = pendingRawDtoBlocks.get(0);
if (nextPending.getHeight() == burningManAccountingService.getBlockHeightOfLastBlock() + 1) {
parseBlock(nextPending);
}
}
return Optional.of(accountingBlock);
} catch (BlockHeightNotConnectingException e) {
// If height of rawDtoBlock is not at expected heightForNextBlock but further in the future we add it to pendingRawDtoBlocks
int heightForNextBlock = burningManAccountingService.getBlockHeightOfLastBlock() + 1;
if (rawDtoBlock.getHeight() > heightForNextBlock && !pendingRawDtoBlocks.contains(rawDtoBlock)) {
pendingRawDtoBlocks.add(rawDtoBlock);
log.info("We received a block with a future block height. We store it as pending and try to apply it at the next block. " +
"heightForNextBlock={}, rawDtoBlock: height/hash={}/{}", heightForNextBlock, rawDtoBlock.getHeight(), rawDtoBlock.getHash());
}
} catch (BlockHashNotConnectingException throwable) {
Optional<AccountingBlock> lastBlock = burningManAccountingService.getLastBlock();
log.warn("Block not connecting:\n" +
"New block height={}; hash={}, previousBlockHash={}, latest block height={}; hash={}",
rawDtoBlock.getHeight(),
rawDtoBlock.getHash(),
rawDtoBlock.getPreviousBlockHash(),
lastBlock.isPresent() ? lastBlock.get().getHeight() : "lastBlock not present",
lastBlock.isPresent() ? lastBlock.get().getTruncatedHash() : "lastBlock not present");
pendingRawDtoBlocks.clear();
applyReOrg();
}
return Optional.empty();
}
private void handleError(Throwable throwable) {
if (throwable instanceof BlockHashNotConnectingException || throwable instanceof BlockHeightNotConnectingException) {
// We do not escalate that exception as it is handled with the snapshot manager to recover its state.
log.warn(throwable.toString());
} else {
String errorMessage = "An error occurred: Error=" + throwable.toString();
log.error(errorMessage);
throwable.printStackTrace();
if (throwable instanceof RpcException) {
Throwable cause = throwable.getCause();
if (cause != null) {
if (cause instanceof ConnectException) {
if (warnMessageHandler != null)
warnMessageHandler.accept("You have configured Bisq to run as BM full node but there is no " +
"localhost Bitcoin Core node detected. You need to have Bitcoin Core started and synced before " +
"starting Bisq. Please restart Bisq with proper BM full node setup or switch to lite node mode.");
return;
} else if (cause instanceof NotificationHandlerException) {
log.error("Error from within block notification daemon: {}", cause.getCause().toString());
applyReOrg();
return;
} else if (cause instanceof Error) {
throw (Error) cause;
}
}
}
if (errorMessageHandler != null)
errorMessageHandler.accept(errorMessage);
}
}
}

View file

@ -0,0 +1,230 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.node.full.network;
import bisq.core.dao.burningman.accounting.BurningManAccountingService;
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
import bisq.core.dao.burningman.accounting.node.AccountingNode;
import bisq.core.dao.burningman.accounting.node.messages.GetAccountingBlocksRequest;
import bisq.core.dao.burningman.accounting.node.messages.NewAccountingBlockBroadcastMessage;
import bisq.network.p2p.network.Connection;
import bisq.network.p2p.network.MessageListener;
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.app.DevEnv;
import bisq.common.config.Config;
import bisq.common.proto.network.NetworkEnvelope;
import org.bitcoinj.core.ECKey;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
import static com.google.common.base.Preconditions.checkArgument;
import static org.bitcoinj.core.Utils.HEX;
// Taken from FullNodeNetworkService
@Singleton
@Slf4j
public class AccountingFullNodeNetworkService implements MessageListener, PeerManager.Listener {
private static final long CLEANUP_TIMER = 120;
private final NetworkNode networkNode;
private final PeerManager peerManager;
private final Broadcaster broadcaster;
private final BurningManAccountingService burningManAccountingService;
private final boolean useDevPrivilegeKeys;
@Nullable
private final String bmOracleNodePubKey;
@Nullable
private final ECKey bmOracleNodePrivKey;
// Key is connection UID
private final Map<String, GetAccountingBlocksRequestHandler> getBlocksRequestHandlers = new HashMap<>();
private boolean stopped;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public AccountingFullNodeNetworkService(NetworkNode networkNode,
PeerManager peerManager,
Broadcaster broadcaster,
BurningManAccountingService burningManAccountingService,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys,
@Named(Config.BM_ORACLE_NODE_PUB_KEY) String bmOracleNodePubKey,
@Named(Config.BM_ORACLE_NODE_PRIV_KEY) String bmOracleNodePrivKey) {
this.networkNode = networkNode;
this.peerManager = peerManager;
this.broadcaster = broadcaster;
this.burningManAccountingService = burningManAccountingService;
this.useDevPrivilegeKeys = useDevPrivilegeKeys;
if (useDevPrivilegeKeys) {
bmOracleNodePubKey = DevEnv.DEV_PRIVILEGE_PUB_KEY;
bmOracleNodePrivKey = DevEnv.DEV_PRIVILEGE_PRIV_KEY;
}
this.bmOracleNodePubKey = bmOracleNodePubKey.isEmpty() ? null : bmOracleNodePubKey;
this.bmOracleNodePrivKey = bmOracleNodePrivKey.isEmpty() ? null : toEcKey(bmOracleNodePrivKey);
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void addListeners() {
networkNode.addMessageListener(this);
peerManager.addListener(this);
}
public void shutDown() {
stopped = true;
networkNode.removeMessageListener(this);
peerManager.removeListener(this);
}
public void publishAccountingBlock(AccountingBlock block) {
if (bmOracleNodePubKey == null || bmOracleNodePrivKey == null) {
log.warn("Ignore publishNewBlock call. bmOracleNodePubKey or bmOracleNodePrivKey are not set up");
return;
}
checkArgument(AccountingNode.isPermittedPubKey(useDevPrivilegeKeys, bmOracleNodePubKey),
"The bmOracleNodePubKey must be included in the hard coded list of supported pub keys");
log.info("Publish new block at height={}", block.getHeight());
byte[] signature = AccountingNode.getSignature(AccountingNode.getSha256Hash(block), bmOracleNodePrivKey);
NewAccountingBlockBroadcastMessage message = new NewAccountingBlockBroadcastMessage(block, bmOracleNodePubKey, signature);
broadcaster.broadcast(message, networkNode.getNodeAddress());
}
///////////////////////////////////////////////////////////////////////////////////////////
// PeerManager.Listener implementation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onAllConnectionsLost() {
stopped = true;
}
@Override
public void onNewConnectionAfterAllConnectionsLost() {
stopped = false;
}
@Override
public void onAwakeFromStandby() {
stopped = false;
}
///////////////////////////////////////////////////////////////////////////////////////////
// MessageListener implementation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
if (networkEnvelope instanceof GetAccountingBlocksRequest) {
handleGetBlocksRequest((GetAccountingBlocksRequest) networkEnvelope, connection);
}
}
private void handleGetBlocksRequest(GetAccountingBlocksRequest getBlocksRequest, Connection connection) {
if (bmOracleNodePubKey == null || bmOracleNodePrivKey == null) {
log.warn("Ignore handleGetBlocksRequest call. bmOracleNodePubKey or bmOracleNodePrivKey are not set up");
return;
}
checkArgument(AccountingNode.isPermittedPubKey(useDevPrivilegeKeys, bmOracleNodePubKey),
"The bmOracleNodePubKey must be included in the hard coded list of supported pub keys");
if (stopped) {
log.warn("We have stopped already. We ignore that onMessage call.");
return;
}
String uid = connection.getUid();
if (getBlocksRequestHandlers.containsKey(uid)) {
log.warn("We have already a GetDataRequestHandler for that connection started. " +
"We start a cleanup timer if the handler has not closed by itself in between 2 minutes.");
UserThread.runAfter(() -> {
if (getBlocksRequestHandlers.containsKey(uid)) {
GetAccountingBlocksRequestHandler handler = getBlocksRequestHandlers.get(uid);
handler.stop();
getBlocksRequestHandlers.remove(uid);
}
}, CLEANUP_TIMER);
return;
}
GetAccountingBlocksRequestHandler requestHandler = new GetAccountingBlocksRequestHandler(networkNode,
burningManAccountingService,
bmOracleNodePrivKey,
bmOracleNodePubKey,
new GetAccountingBlocksRequestHandler.Listener() {
@Override
public void onComplete() {
getBlocksRequestHandlers.remove(uid);
}
@Override
public void onFault(String errorMessage, @Nullable Connection connection) {
getBlocksRequestHandlers.remove(uid);
if (!stopped) {
log.trace("GetDataRequestHandler failed.\n\tConnection={}\n\t" +
"ErrorMessage={}", connection, errorMessage);
if (connection != null) {
peerManager.handleConnectionFault(connection);
}
} else {
log.warn("We have stopped already. We ignore that getDataRequestHandler.handle.onFault call.");
}
}
});
getBlocksRequestHandlers.put(uid, requestHandler);
requestHandler.onGetBlocksRequest(getBlocksRequest, connection);
}
private ECKey toEcKey(String bmOracleNodePrivKey) {
try {
return ECKey.fromPrivate(new BigInteger(1, HEX.decode(bmOracleNodePrivKey)));
} catch (Throwable t) {
log.error("Error at creating EC key out of bmOracleNodePrivKey", t);
return null;
}
}
}

View file

@ -0,0 +1,171 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.node.full.network;
import bisq.core.dao.burningman.accounting.BurningManAccountingService;
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
import bisq.core.dao.burningman.accounting.node.AccountingNode;
import bisq.core.dao.burningman.accounting.node.messages.GetAccountingBlocksRequest;
import bisq.core.dao.burningman.accounting.node.messages.GetAccountingBlocksResponse;
import bisq.network.p2p.network.CloseConnectionReason;
import bisq.network.p2p.network.Connection;
import bisq.network.p2p.network.NetworkNode;
import bisq.common.Timer;
import bisq.common.UserThread;
import org.bitcoinj.core.ECKey;
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.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
// Taken from GetBlocksRequestHandler
@Slf4j
class GetAccountingBlocksRequestHandler {
private static final long TIMEOUT_MIN = 3;
///////////////////////////////////////////////////////////////////////////////////////////
// Listener
///////////////////////////////////////////////////////////////////////////////////////////
public interface Listener {
void onComplete();
void onFault(String errorMessage, Connection connection);
}
private final NetworkNode networkNode;
private final BurningManAccountingService burningManAccountingService;
private final ECKey bmOracleNodePrivKey;
private final String bmOracleNodePubKey;
private final Listener listener;
private Timer timeoutTimer;
private boolean stopped;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
GetAccountingBlocksRequestHandler(NetworkNode networkNode,
BurningManAccountingService burningManAccountingService,
ECKey bmOracleNodePrivKey,
String bmOracleNodePubKey,
Listener listener) {
this.networkNode = networkNode;
this.burningManAccountingService = burningManAccountingService;
this.bmOracleNodePrivKey = bmOracleNodePrivKey;
this.bmOracleNodePubKey = bmOracleNodePubKey;
this.listener = listener;
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void onGetBlocksRequest(GetAccountingBlocksRequest request, Connection connection) {
long ts = System.currentTimeMillis();
List<AccountingBlock> blocks = burningManAccountingService.getBlocks().stream()
.filter(block -> block.getHeight() >= request.getFromBlockHeight())
.collect(Collectors.toList());
byte[] signature = AccountingNode.getSignature(AccountingNode.getSha256Hash(blocks), bmOracleNodePrivKey);
GetAccountingBlocksResponse getBlocksResponse = new GetAccountingBlocksResponse(blocks, request.getNonce(), bmOracleNodePubKey, signature);
log.info("Received GetAccountingBlocksRequest from {} for blocks from height {}. " +
"Building GetAccountingBlocksResponse with {} blocks took {} ms.",
connection.getPeersNodeAddressOptional(), request.getFromBlockHeight(),
blocks.size(), System.currentTimeMillis() - ts);
if (timeoutTimer != null) {
timeoutTimer.stop();
log.warn("Timeout was already running. We stopped it.");
}
timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions
String errorMessage = "A timeout occurred for getBlocksResponse.requestNonce:" +
getBlocksResponse.getRequestNonce() +
" on connection: " + connection;
handleFault(errorMessage, CloseConnectionReason.SEND_MSG_TIMEOUT, connection);
},
TIMEOUT_MIN, TimeUnit.MINUTES);
SettableFuture<Connection> future = networkNode.sendMessage(connection, getBlocksResponse);
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(Connection connection) {
if (!stopped) {
log.info("Send DataResponse to {} succeeded. getBlocksResponse.getBlocks().size()={}",
connection.getPeersNodeAddressOptional(), getBlocksResponse.getBlocks().size());
cleanup();
listener.onComplete();
} else {
log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call.");
}
}
@Override
public void onFailure(@NotNull Throwable throwable) {
if (!stopped) {
String errorMessage = "Sending getBlocksResponse to " + connection +
" failed. That is expected if the peer is offline. getBlocksResponse=" + getBlocksResponse + "." +
"Exception: " + throwable.getMessage();
handleFault(errorMessage, CloseConnectionReason.SEND_MSG_FAILURE, connection);
} else {
log.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call.");
}
}
}, MoreExecutors.directExecutor());
}
public void stop() {
cleanup();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void handleFault(String errorMessage, CloseConnectionReason closeConnectionReason, Connection connection) {
if (!stopped) {
log.warn("{}, closeConnectionReason={}", errorMessage, closeConnectionReason);
cleanup();
listener.onFault(errorMessage, connection);
} else {
log.warn("We have already stopped (handleFault)");
}
}
private void cleanup() {
stopped = true;
if (timeoutTimer != null) {
timeoutTimer.stop();
timeoutTimer = null;
}
}
}

View file

@ -0,0 +1,310 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.node.lite;
import bisq.core.btc.setup.WalletsSetup;
import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.dao.burningman.accounting.BurningManAccountingService;
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
import bisq.core.dao.burningman.accounting.exceptions.BlockHashNotConnectingException;
import bisq.core.dao.burningman.accounting.exceptions.BlockHeightNotConnectingException;
import bisq.core.dao.burningman.accounting.node.AccountingNode;
import bisq.core.dao.burningman.accounting.node.full.AccountingBlockParser;
import bisq.core.dao.burningman.accounting.node.lite.network.AccountingLiteNodeNetworkService;
import bisq.core.dao.burningman.accounting.node.messages.GetAccountingBlocksResponse;
import bisq.core.dao.burningman.accounting.node.messages.NewAccountingBlockBroadcastMessage;
import bisq.core.dao.state.DaoStateService;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.network.ConnectionState;
import bisq.common.Timer;
import bisq.common.UserThread;
import bisq.common.config.Config;
import bisq.common.util.Hex;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import javafx.beans.value.ChangeListener;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Singleton
public class AccountingLiteNode extends AccountingNode implements AccountingLiteNodeNetworkService.Listener {
private static final int CHECK_FOR_BLOCK_RECEIVED_DELAY_SEC = 10;
private final WalletsSetup walletsSetup;
private final BsqWalletService bsqWalletService;
private final AccountingLiteNodeNetworkService accountingLiteNodeNetworkService;
private final boolean useDevPrivilegeKeys;
private final List<AccountingBlock> pendingAccountingBlocks = new ArrayList<>();
private final ChangeListener<Number> blockDownloadListener;
private Timer checkForBlockReceivedTimer;
private int requestBlocksCounter;
@Inject
public AccountingLiteNode(P2PService p2PService,
DaoStateService daoStateService,
BurningManAccountingService burningManAccountingService,
AccountingBlockParser accountingBlockParser,
WalletsSetup walletsSetup,
BsqWalletService bsqWalletService,
AccountingLiteNodeNetworkService accountingLiteNodeNetworkService,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
super(p2PService, daoStateService, burningManAccountingService, accountingBlockParser);
this.walletsSetup = walletsSetup;
this.bsqWalletService = bsqWalletService;
this.accountingLiteNodeNetworkService = accountingLiteNodeNetworkService;
this.useDevPrivilegeKeys = useDevPrivilegeKeys;
blockDownloadListener = (observable, oldValue, newValue) -> {
if ((double) newValue == 1) {
setupWalletBestBlockListener();
}
};
}
///////////////////////////////////////////////////////////////////////////////////////////
// Public methods
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void start() {
// We do not start yes but wait until DAO block parsing is complete to not interfere with
// that higher priority activity.
}
@Override
public void shutDown() {
accountingLiteNodeNetworkService.shutDown();
}
///////////////////////////////////////////////////////////////////////////////////////////
// AccountingLiteNodeNetworkService.Listener
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onRequestedBlocksReceived(GetAccountingBlocksResponse getBlocksResponse) {
List<AccountingBlock> blocks = getBlocksResponse.getBlocks();
if (!blocks.isEmpty() && isValidPubKeyAndSignature(AccountingNode.getSha256Hash(blocks),
getBlocksResponse.getPubKey(),
getBlocksResponse.getSignature(),
useDevPrivilegeKeys)) {
processAccountingBlocks(blocks);
}
}
@Override
public void onNewBlockReceived(NewAccountingBlockBroadcastMessage message) {
AccountingBlock accountingBlock = message.getBlock();
if (isValidPubKeyAndSignature(AccountingNode.getSha256Hash(accountingBlock),
message.getPubKey(),
message.getSignature(),
useDevPrivilegeKeys)) {
processNewAccountingBlock(accountingBlock);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Protected methods
///////////////////////////////////////////////////////////////////////////////////////////
// We start after initial DAO parsing is complete
@Override
protected void onInitialDaoBlockParsingComplete() {
accountingLiteNodeNetworkService.addListeners();
// We wait until the wallet is synced before using it for triggering requests
if (walletsSetup.isDownloadComplete()) {
setupWalletBestBlockListener();
} else {
walletsSetup.downloadPercentageProperty().addListener(blockDownloadListener);
}
super.onInitialized();
}
@Override
protected void onP2PNetworkReady() {
super.onP2PNetworkReady();
accountingLiteNodeNetworkService.addListener(this);
if (!initialBlockRequestsComplete) {
startRequestBlocks();
}
}
@Override
protected void startRequestBlocks() {
int heightOfLastBlock = burningManAccountingService.getBlockHeightOfLastBlock();
if (walletsSetup.isDownloadComplete() && heightOfLastBlock == bsqWalletService.getBestChainHeight()) {
log.info("No block request needed as we have already the most recent block. " +
"heightOfLastBlock={}, bsqWalletService.getBestChainHeight()={}",
heightOfLastBlock, bsqWalletService.getBestChainHeight());
onInitialBlockRequestsComplete();
return;
}
ConnectionState.incrementExpectedInitialDataResponses();
accountingLiteNodeNetworkService.requestBlocks(heightOfLastBlock + 1);
}
@Override
protected void applyReOrg() {
pendingAccountingBlocks.clear();
accountingLiteNodeNetworkService.reset();
super.applyReOrg();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void processAccountingBlocks(List<AccountingBlock> blocks) {
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;
}
}
if (requiresReOrg) {
applyReOrg();
return;
}
int heightOfLastBlock = burningManAccountingService.getBlockHeightOfLastBlock();
if (walletsSetup.isDownloadComplete() && heightOfLastBlock < bsqWalletService.getBestChainHeight()) {
accountingLiteNodeNetworkService.requestBlocks(heightOfLastBlock + 1);
} else {
if (!initialBlockRequestsComplete) {
onInitialBlockRequestsComplete();
}
}
}
private void processNewAccountingBlock(AccountingBlock accountingBlock) {
int blockHeight = accountingBlock.getHeight();
log.info("onNewBlockReceived: accountingBlock at height {}", blockHeight);
pendingAccountingBlocks.remove(accountingBlock);
try {
burningManAccountingService.addBlock(accountingBlock);
burningManAccountingService.onNewBlockReceived(accountingBlock);
// After parsing we check if we have pending blocks we might have received earlier but which have been
// not connecting from the latest height we had. The list is sorted by height
if (!pendingAccountingBlocks.isEmpty()) {
// We take only first element after sorting (so it is the accountingBlock with the next height) to avoid that
// we would repeat calls in recursions in case we would iterate the list.
pendingAccountingBlocks.sort(Comparator.comparing(AccountingBlock::getHeight));
AccountingBlock nextPending = pendingAccountingBlocks.get(0);
if (nextPending.getHeight() == burningManAccountingService.getBlockHeightOfLastBlock() + 1) {
processNewAccountingBlock(nextPending);
}
}
} catch (BlockHeightNotConnectingException e) {
// If height of rawDtoBlock is not at expected heightForNextBlock but further in the future we add it to pendingRawDtoBlocks
int heightForNextBlock = burningManAccountingService.getBlockHeightOfLastBlock() + 1;
if (accountingBlock.getHeight() > heightForNextBlock && !pendingAccountingBlocks.contains(accountingBlock)) {
pendingAccountingBlocks.add(accountingBlock);
log.info("We received a accountingBlock with a future accountingBlock height. We store it as pending and try to apply it at the next accountingBlock. " +
"heightForNextBlock={}, accountingBlock: height/truncatedHash={}/{}", heightForNextBlock, accountingBlock.getHeight(), accountingBlock.getTruncatedHash());
requestBlocksCounter++;
log.warn("We are trying to call requestBlocks with heightForNextBlock {} after a delay of {} min.",
heightForNextBlock, requestBlocksCounter * requestBlocksCounter);
if (requestBlocksCounter <= 5) {
UserThread.runAfter(() -> {
pendingAccountingBlocks.clear();
accountingLiteNodeNetworkService.requestBlocks(heightForNextBlock);
},
requestBlocksCounter * requestBlocksCounter * 60L);
} else {
log.warn("We tried {} times to call requestBlocks with heightForNextBlock {}.",
requestBlocksCounter, heightForNextBlock);
}
}
} catch (BlockHashNotConnectingException e) {
Optional<AccountingBlock> lastBlock = burningManAccountingService.getLastBlock();
log.warn("Block not connecting:\n" +
"New block height/hash/previousBlockHash={}/{}/{}, latest block height/hash={}/{}",
accountingBlock.getHeight(),
Hex.encode(accountingBlock.getTruncatedHash()),
Hex.encode(accountingBlock.getTruncatedPreviousBlockHash()),
lastBlock.isPresent() ? lastBlock.get().getHeight() : "lastBlock not present",
lastBlock.isPresent() ? Hex.encode(lastBlock.get().getTruncatedHash()) : "lastBlock not present");
applyReOrg();
}
}
private void setupWalletBestBlockListener() {
walletsSetup.downloadPercentageProperty().removeListener(blockDownloadListener);
bsqWalletService.addNewBestBlockListener(blockFromWallet -> {
// If we are not completed with initial block requests we return
if (!initialBlockRequestsComplete)
return;
if (checkForBlockReceivedTimer != null) {
// In case we received a new block before out timer gets called we stop the old timer
checkForBlockReceivedTimer.stop();
}
int walletBlockHeight = blockFromWallet.getHeight();
log.info("New block at height {} from bsqWalletService", walletBlockHeight);
// We expect to receive the new BSQ block from the network shortly after BitcoinJ has been aware of it.
// If we don't receive it we request it manually from seed nodes
checkForBlockReceivedTimer = UserThread.runAfter(() -> {
int heightOfLastBlock = burningManAccountingService.getBlockHeightOfLastBlock();
if (heightOfLastBlock < walletBlockHeight) {
log.warn("We did not receive a block from the network {} seconds after we saw the new block in BitcoinJ. " +
"We request from our seed nodes missing blocks from block height {}.",
CHECK_FOR_BLOCK_RECEIVED_DELAY_SEC, heightOfLastBlock + 1);
accountingLiteNodeNetworkService.requestBlocks(heightOfLastBlock + 1);
}
}, CHECK_FOR_BLOCK_RECEIVED_DELAY_SEC);
});
}
}

View file

@ -0,0 +1,392 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.node.lite.network;
import bisq.core.dao.burningman.accounting.node.messages.GetAccountingBlocksResponse;
import bisq.core.dao.burningman.accounting.node.messages.NewAccountingBlockBroadcastMessage;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.CloseConnectionReason;
import bisq.network.p2p.network.Connection;
import bisq.network.p2p.network.ConnectionListener;
import bisq.network.p2p.network.MessageListener;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.peers.Broadcaster;
import bisq.network.p2p.peers.PeerManager;
import bisq.network.p2p.seed.SeedNodeRepository;
import bisq.network.p2p.storage.P2PDataStorage;
import bisq.common.Timer;
import bisq.common.UserThread;
import bisq.common.app.DevEnv;
import bisq.common.proto.network.NetworkEnvelope;
import bisq.common.util.Tuple2;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
// Taken from LiteNodeNetworkService
@Slf4j
@Singleton
public class AccountingLiteNodeNetworkService implements MessageListener, ConnectionListener, PeerManager.Listener {
private static final long RETRY_DELAY_SEC = 10;
private static final long CLEANUP_TIMER = 120;
private static final int MAX_RETRY = 12;
private int retryCounter = 0;
private int lastRequestedBlockHeight;
private int lastReceivedBlockHeight;
///////////////////////////////////////////////////////////////////////////////////////////
// Listener
///////////////////////////////////////////////////////////////////////////////////////////
public interface Listener {
void onRequestedBlocksReceived(GetAccountingBlocksResponse getBlocksResponse);
void onNewBlockReceived(NewAccountingBlockBroadcastMessage newBlockBroadcastMessage);
}
private final NetworkNode networkNode;
private final PeerManager peerManager;
private final Broadcaster broadcaster;
private final Collection<NodeAddress> seedNodeAddresses;
private final List<Listener> listeners = new CopyOnWriteArrayList<>();
// Key is tuple of seedNode address and requested blockHeight
private final Map<Tuple2<NodeAddress, Integer>, RequestAccountingBlocksHandler> requestBlocksHandlerMap = new HashMap<>();
private Timer retryTimer;
private boolean stopped;
private final Set<P2PDataStorage.ByteArray> receivedBlockMessages = new HashSet<>();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public AccountingLiteNodeNetworkService(NetworkNode networkNode,
PeerManager peerManager,
Broadcaster broadcaster,
SeedNodeRepository seedNodesRepository) {
this.networkNode = networkNode;
this.peerManager = peerManager;
this.broadcaster = broadcaster;
// seedNodeAddresses can be empty (in case there is only 1 seed node, the seed node starting up has no other seed nodes)
this.seedNodeAddresses = new HashSet<>(seedNodesRepository.getSeedNodeAddresses());
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void addListeners() {
networkNode.addMessageListener(this);
networkNode.addConnectionListener(this);
peerManager.addListener(this);
}
public void shutDown() {
stopped = true;
stopRetryTimer();
networkNode.removeMessageListener(this);
networkNode.removeConnectionListener(this);
peerManager.removeListener(this);
closeAllHandlers();
}
public void addListener(Listener listener) {
listeners.add(listener);
}
public void requestBlocks(int startBlockHeight) {
lastRequestedBlockHeight = startBlockHeight;
Optional<Connection> connectionToSeedNodeOptional = networkNode.getConfirmedConnections().stream()
.filter(peerManager::isSeedNode)
.findAny();
connectionToSeedNodeOptional.flatMap(Connection::getPeersNodeAddressOptional)
.ifPresentOrElse(candidate -> {
seedNodeAddresses.remove(candidate);
requestBlocks(candidate, startBlockHeight);
}, () -> {
tryWithNewSeedNode(startBlockHeight);
});
}
public void reset() {
lastRequestedBlockHeight = 0;
lastReceivedBlockHeight = 0;
retryCounter = 0;
requestBlocksHandlerMap.values().forEach(RequestAccountingBlocksHandler::terminate);
requestBlocksHandlerMap.clear();
}
///////////////////////////////////////////////////////////////////////////////////////////
// ConnectionListener implementation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onConnection(Connection connection) {
}
@Override
public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) {
closeHandler(connection);
if (peerManager.isPeerBanned(closeConnectionReason, connection)) {
connection.getPeersNodeAddressOptional().ifPresent(nodeAddress -> {
seedNodeAddresses.remove(nodeAddress);
removeFromRequestBlocksHandlerMap(nodeAddress);
});
}
}
@Override
public void onError(Throwable throwable) {
}
///////////////////////////////////////////////////////////////////////////////////////////
// PeerManager.Listener implementation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onAllConnectionsLost() {
log.info("onAllConnectionsLost");
closeAllHandlers();
stopRetryTimer();
stopped = true;
tryWithNewSeedNode(lastRequestedBlockHeight);
}
@Override
public void onNewConnectionAfterAllConnectionsLost() {
log.info("onNewConnectionAfterAllConnectionsLost");
closeAllHandlers();
stopped = false;
tryWithNewSeedNode(lastRequestedBlockHeight);
}
@Override
public void onAwakeFromStandby() {
log.info("onAwakeFromStandby");
closeAllHandlers();
stopped = false;
tryWithNewSeedNode(lastRequestedBlockHeight);
}
///////////////////////////////////////////////////////////////////////////////////////////
// MessageListener implementation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
if (networkEnvelope instanceof NewAccountingBlockBroadcastMessage) {
NewAccountingBlockBroadcastMessage newBlockBroadcastMessage = (NewAccountingBlockBroadcastMessage) networkEnvelope;
P2PDataStorage.ByteArray truncatedHash = new P2PDataStorage.ByteArray(newBlockBroadcastMessage.getBlock().getTruncatedHash());
if (receivedBlockMessages.contains(truncatedHash)) {
log.debug("We had that message already and do not further broadcast it. hash={}", truncatedHash);
return;
}
log.info("We received a NewAccountingBlockBroadcastMessage from peer {} and broadcast it to our peers. height={} truncatedHash={}",
connection.getPeersNodeAddressOptional().orElse(null), newBlockBroadcastMessage.getBlock().getHeight(), truncatedHash);
receivedBlockMessages.add(truncatedHash);
broadcaster.broadcast(newBlockBroadcastMessage, connection.getPeersNodeAddressOptional().orElse(null));
listeners.forEach(listener -> listener.onNewBlockReceived(newBlockBroadcastMessage));
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// RequestData
///////////////////////////////////////////////////////////////////////////////////////////
private void requestBlocks(NodeAddress peersNodeAddress, int startBlockHeight) {
if (stopped) {
log.warn("We have stopped already. We ignore that requestData call.");
return;
}
Tuple2<NodeAddress, Integer> key = new Tuple2<>(peersNodeAddress, startBlockHeight);
if (requestBlocksHandlerMap.containsKey(key)) {
log.warn("We have started already a requestDataHandshake for startBlockHeight {} to peer. nodeAddress={}\n" +
"We start a cleanup timer if the handler has not closed by itself in between 2 minutes.",
peersNodeAddress, startBlockHeight);
UserThread.runAfter(() -> {
if (requestBlocksHandlerMap.containsKey(key)) {
RequestAccountingBlocksHandler handler = requestBlocksHandlerMap.get(key);
handler.terminate();
requestBlocksHandlerMap.remove(key);
}
}, CLEANUP_TIMER);
return;
}
if (startBlockHeight < lastReceivedBlockHeight) {
log.warn("startBlockHeight must not be smaller than lastReceivedBlockHeight. That should never happen." +
"startBlockHeight={},lastReceivedBlockHeight={}", startBlockHeight, lastReceivedBlockHeight);
DevEnv.logErrorAndThrowIfDevMode("startBlockHeight must be larger than lastReceivedBlockHeight. startBlockHeight=" +
startBlockHeight + " / lastReceivedBlockHeight=" + lastReceivedBlockHeight);
return;
}
RequestAccountingBlocksHandler requestAccountingBlocksHandler = new RequestAccountingBlocksHandler(networkNode,
peerManager,
peersNodeAddress,
startBlockHeight,
new RequestAccountingBlocksHandler.Listener() {
@Override
public void onComplete(GetAccountingBlocksResponse getBlocksResponse) {
log.info("requestBlocksHandler to {} completed", peersNodeAddress);
stopRetryTimer();
// need to remove before listeners are notified as they cause the update call
requestBlocksHandlerMap.remove(key);
// we only notify if our request was latest
if (startBlockHeight > lastReceivedBlockHeight) {
lastReceivedBlockHeight = startBlockHeight;
listeners.forEach(listener -> listener.onRequestedBlocksReceived(getBlocksResponse));
} else {
log.warn("We got a response which is already obsolete because we received a " +
"response from a request with same or higher block height.");
}
}
@Override
public void onFault(String errorMessage, @Nullable Connection connection) {
log.warn("requestBlocksHandler with outbound connection failed.\n\tnodeAddress={}\n\t" +
"ErrorMessage={}", peersNodeAddress, errorMessage);
peerManager.handleConnectionFault(peersNodeAddress);
requestBlocksHandlerMap.remove(key);
tryWithNewSeedNode(startBlockHeight);
}
});
requestBlocksHandlerMap.put(key, requestAccountingBlocksHandler);
requestAccountingBlocksHandler.requestBlocks();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
private void tryWithNewSeedNode(int startBlockHeight) {
if (networkNode.getAllConnections().isEmpty()) {
return;
}
if (lastRequestedBlockHeight == 0) {
return;
}
if (stopped) {
return;
}
if (retryTimer != null) {
log.warn("We have a retry timer already running.");
return;
}
retryCounter++;
if (retryCounter > MAX_RETRY) {
log.warn("We tried {} times but could not connect to a seed node.", retryCounter);
return;
}
retryTimer = UserThread.runAfter(() -> {
stopped = false;
stopRetryTimer();
List<NodeAddress> list = seedNodeAddresses.stream()
.filter(e -> peerManager.isSeedNode(e) && !peerManager.isSelf(e))
.collect(Collectors.toList());
Collections.shuffle(list);
if (!list.isEmpty()) {
NodeAddress nextCandidate = list.get(0);
seedNodeAddresses.remove(nextCandidate);
log.info("We try requestBlocks from {} with startBlockHeight={}", nextCandidate, startBlockHeight);
requestBlocks(nextCandidate, startBlockHeight);
} else {
log.warn("No more seed nodes available we could try.");
}
},
RETRY_DELAY_SEC);
}
private void stopRetryTimer() {
if (retryTimer != null) {
retryTimer.stop();
retryTimer = null;
}
}
private void closeHandler(Connection connection) {
Optional<NodeAddress> peersNodeAddressOptional = connection.getPeersNodeAddressOptional();
if (peersNodeAddressOptional.isPresent()) {
NodeAddress nodeAddress = peersNodeAddressOptional.get();
removeFromRequestBlocksHandlerMap(nodeAddress);
} else {
log.trace("closeHandler: nodeAddress not set in connection {}", connection);
}
}
private void removeFromRequestBlocksHandlerMap(NodeAddress nodeAddress) {
requestBlocksHandlerMap.entrySet().stream()
.filter(e -> e.getKey().first.equals(nodeAddress))
.findAny()
.ifPresent(e -> {
e.getValue().terminate();
requestBlocksHandlerMap.remove(e.getKey());
});
}
private void closeAllHandlers() {
requestBlocksHandlerMap.values().forEach(RequestAccountingBlocksHandler::terminate);
requestBlocksHandlerMap.clear();
}
}

View file

@ -0,0 +1,219 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.node.lite.network;
import bisq.core.dao.burningman.accounting.node.messages.GetAccountingBlocksRequest;
import bisq.core.dao.burningman.accounting.node.messages.GetAccountingBlocksResponse;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.CloseConnectionReason;
import bisq.network.p2p.network.Connection;
import bisq.network.p2p.network.MessageListener;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.peers.PeerManager;
import bisq.common.Timer;
import bisq.common.UserThread;
import bisq.common.proto.network.NetworkEnvelope;
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.Optional;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
// Taken from RequestBlocksHandler
@Slf4j
public class RequestAccountingBlocksHandler implements MessageListener {
private static final long TIMEOUT_MIN = 3;
///////////////////////////////////////////////////////////////////////////////////////////
// Listener
///////////////////////////////////////////////////////////////////////////////////////////
public interface Listener {
void onComplete(GetAccountingBlocksResponse getBlocksResponse);
@SuppressWarnings("UnusedParameters")
void onFault(String errorMessage, @SuppressWarnings("SameParameterValue") @Nullable Connection connection);
}
private final NetworkNode networkNode;
private final PeerManager peerManager;
@Getter
private final NodeAddress nodeAddress;
@Getter
private final int startBlockHeight;
private final Listener listener;
private Timer timeoutTimer;
private final int nonce = new Random().nextInt();
private boolean stopped;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public RequestAccountingBlocksHandler(NetworkNode networkNode,
PeerManager peerManager,
NodeAddress nodeAddress,
int startBlockHeight,
Listener listener) {
this.networkNode = networkNode;
this.peerManager = peerManager;
this.nodeAddress = nodeAddress;
this.startBlockHeight = startBlockHeight;
this.listener = listener;
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void requestBlocks() {
if (stopped) {
log.warn("We have stopped already. We ignore that requestData call.");
return;
}
GetAccountingBlocksRequest getBlocksRequest = new GetAccountingBlocksRequest(startBlockHeight, nonce, networkNode.getNodeAddress());
if (timeoutTimer != null) {
log.warn("We had a timer already running and stop it.");
timeoutTimer.stop();
}
timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions
if (!stopped) {
String errorMessage = "A timeout occurred when sending getBlocksRequest:" + getBlocksRequest +
" on peersNodeAddress:" + nodeAddress;
log.debug("{} / RequestDataHandler={}", errorMessage, RequestAccountingBlocksHandler.this);
handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_TIMEOUT);
} else {
log.warn("We have stopped already. We ignore that timeoutTimer.run call. " +
"Might be caused by a previous networkNode.sendMessage.onFailure.");
}
},
TIMEOUT_MIN, TimeUnit.MINUTES);
log.info("We request blocks from peer {} from block height {}.", nodeAddress, getBlocksRequest.getFromBlockHeight());
networkNode.addMessageListener(this);
SettableFuture<Connection> future = networkNode.sendMessage(nodeAddress, getBlocksRequest);
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(Connection connection) {
log.info("Sending of GetAccountingBlocksRequest message to peer {} succeeded.", nodeAddress.getFullAddress());
}
@Override
public void onFailure(@NotNull Throwable throwable) {
if (!stopped) {
String errorMessage = "Sending getBlocksRequest to " + nodeAddress +
" failed. That is expected if the peer is offline.\n\t" +
"getBlocksRequest=" + getBlocksRequest + "." +
"\n\tException=" + throwable.getMessage();
log.error(errorMessage);
handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_FAILURE);
} else {
log.warn("We have stopped already. We ignore that networkNode.sendMessage.onFailure call.");
}
}
}, MoreExecutors.directExecutor());
}
///////////////////////////////////////////////////////////////////////////////////////////
// MessageListener implementation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
if (networkEnvelope instanceof GetAccountingBlocksResponse) {
if (stopped) {
log.warn("We have stopped already. We ignore that onDataRequest call.");
return;
}
Optional<NodeAddress> optionalNodeAddress = connection.getPeersNodeAddressOptional();
if (optionalNodeAddress.isEmpty()) {
log.warn("Peers node address is not present, that is not expected.");
// We do not return here as in case the connection has been created from the peers side we might not
// have the address set. As we check the nonce later we do not care that much for the check if the
// connection address is the same as the one we used.
} else if (!optionalNodeAddress.get().equals(nodeAddress)) {
log.warn("Peers node address is not the same we used for the request. This is not expected. We ignore that message.");
return;
}
GetAccountingBlocksResponse getBlocksResponse = (GetAccountingBlocksResponse) networkEnvelope;
if (getBlocksResponse.getRequestNonce() != nonce) {
log.warn("Nonce not matching. That can happen rarely if we get a response after a canceled " +
"handshake (timeout causes connection close but peer might have sent a msg before " +
"connection was closed).\n\t" +
"We drop that message. nonce={} / requestNonce={}",
nonce, getBlocksResponse.getRequestNonce());
return;
}
terminate();
log.info("We received from peer {} a BlocksResponse with {} blocks",
nodeAddress.getFullAddress(), getBlocksResponse.getBlocks().size());
listener.onComplete(getBlocksResponse);
}
}
public void terminate() {
stopped = true;
networkNode.removeMessageListener(this);
stopTimeoutTimer();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
@SuppressWarnings("UnusedParameters")
private void handleFault(String errorMessage,
NodeAddress nodeAddress,
CloseConnectionReason closeConnectionReason) {
terminate();
peerManager.handleConnectionFault(nodeAddress);
listener.onFault(errorMessage, null);
}
private void stopTimeoutTimer() {
if (timeoutTimer != null) {
timeoutTimer.stop();
timeoutTimer = null;
}
}
}

View file

@ -0,0 +1,111 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.node.messages;
import bisq.network.p2p.DirectMessage;
import bisq.network.p2p.InitialDataRequest;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.SendersNodeAddressMessage;
import bisq.network.p2p.SupportedCapabilitiesMessage;
import bisq.common.app.Capabilities;
import bisq.common.app.Version;
import bisq.common.proto.network.NetworkEnvelope;
import java.util.Optional;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
// Taken from GetBlocksRequest
@EqualsAndHashCode(callSuper = true)
@Getter
@Slf4j
public final class GetAccountingBlocksRequest extends NetworkEnvelope implements DirectMessage, SendersNodeAddressMessage,
SupportedCapabilitiesMessage, InitialDataRequest {
private final int fromBlockHeight;
private final int nonce;
private final NodeAddress senderNodeAddress;
private final Capabilities supportedCapabilities;
public GetAccountingBlocksRequest(int fromBlockHeight,
int nonce,
NodeAddress senderNodeAddress) {
this(fromBlockHeight,
nonce,
senderNodeAddress,
Capabilities.app,
Version.getP2PMessageVersion());
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private GetAccountingBlocksRequest(int fromBlockHeight,
int nonce,
NodeAddress senderNodeAddress,
Capabilities supportedCapabilities,
int messageVersion) {
super(messageVersion);
this.fromBlockHeight = fromBlockHeight;
this.nonce = nonce;
this.senderNodeAddress = senderNodeAddress;
this.supportedCapabilities = supportedCapabilities;
}
//todo
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
protobuf.GetAccountingBlocksRequest.Builder builder = protobuf.GetAccountingBlocksRequest.newBuilder()
.setFromBlockHeight(fromBlockHeight)
.setNonce(nonce);
Optional.ofNullable(senderNodeAddress).ifPresent(e -> builder.setSenderNodeAddress(e.toProtoMessage()));
Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities)));
return getNetworkEnvelopeBuilder().setGetAccountingBlocksRequest(builder).build();
}
public static NetworkEnvelope fromProto(protobuf.GetAccountingBlocksRequest proto, int messageVersion) {
protobuf.NodeAddress protoNodeAddress = proto.getSenderNodeAddress();
NodeAddress senderNodeAddress = protoNodeAddress.getHostName().isEmpty() ?
null :
NodeAddress.fromProto(protoNodeAddress);
Capabilities supportedCapabilities = proto.getSupportedCapabilitiesList().isEmpty() ?
null :
Capabilities.fromIntList(proto.getSupportedCapabilitiesList());
return new GetAccountingBlocksRequest(proto.getFromBlockHeight(),
proto.getNonce(),
senderNodeAddress,
supportedCapabilities,
messageVersion);
}
@Override
public String toString() {
return "GetAccountingBlocksRequest{" +
"\n fromBlockHeight=" + fromBlockHeight +
",\n nonce=" + nonce +
",\n senderNodeAddress=" + senderNodeAddress +
",\n supportedCapabilities=" + supportedCapabilities +
"\n} " + super.toString();
}
}

View file

@ -0,0 +1,110 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.node.messages;
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
import bisq.network.p2p.DirectMessage;
import bisq.network.p2p.ExtendedDataSizePermission;
import bisq.network.p2p.InitialDataRequest;
import bisq.network.p2p.InitialDataResponse;
import bisq.common.app.Version;
import bisq.common.proto.network.NetworkEnvelope;
import com.google.protobuf.ByteString;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
// Taken from GetBlocksResponse
@EqualsAndHashCode(callSuper = true)
@Getter
@Slf4j
public final class GetAccountingBlocksResponse extends NetworkEnvelope implements DirectMessage,
ExtendedDataSizePermission, InitialDataResponse {
private final List<AccountingBlock> blocks;
private final int requestNonce;
private final String pubKey;
private final byte[] signature;
public GetAccountingBlocksResponse(List<AccountingBlock> blocks,
int requestNonce,
String pubKey,
byte[] signature) {
this(blocks, requestNonce, pubKey, signature, Version.getP2PMessageVersion());
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private GetAccountingBlocksResponse(List<AccountingBlock> blocks,
int requestNonce,
String pubKey,
byte[] signature,
int messageVersion) {
super(messageVersion);
this.blocks = blocks;
this.requestNonce = requestNonce;
this.pubKey = pubKey;
this.signature = signature;
}
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
protobuf.NetworkEnvelope proto = getNetworkEnvelopeBuilder()
.setGetAccountingBlocksResponse(protobuf.GetAccountingBlocksResponse.newBuilder()
.addAllBlocks(blocks.stream()
.map(AccountingBlock::toProtoMessage)
.collect(Collectors.toList()))
.setRequestNonce(requestNonce)
.setPubKey(pubKey)
.setSignature(ByteString.copyFrom(signature)))
.build();
log.info("Sending a GetBlocksResponse with {} kB", proto.getSerializedSize() / 1000d);
return proto;
}
public static NetworkEnvelope fromProto(protobuf.GetAccountingBlocksResponse proto, int messageVersion) {
List<AccountingBlock> list = proto.getBlocksList().stream()
.map(AccountingBlock::fromProto)
.collect(Collectors.toList());
log.info("Received a GetBlocksResponse with {} blocks and {} kB size", list.size(), proto.getSerializedSize() / 1000d);
return new GetAccountingBlocksResponse(proto.getBlocksList().isEmpty() ?
new ArrayList<>() :
list,
proto.getRequestNonce(),
proto.getPubKey(),
proto.getSignature().toByteArray(),
messageVersion);
}
@Override
public Class<? extends InitialDataRequest> associatedRequest() {
return GetAccountingBlocksRequest.class;
}
}

View file

@ -0,0 +1,78 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.node.messages;
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
import bisq.network.p2p.storage.messages.BroadcastMessage;
import bisq.common.app.Version;
import bisq.common.proto.network.NetworkEnvelope;
import com.google.protobuf.ByteString;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@EqualsAndHashCode(callSuper = true)
@Getter
public final class NewAccountingBlockBroadcastMessage extends BroadcastMessage {
private final AccountingBlock block;
private final String pubKey;
private final byte[] signature;
public NewAccountingBlockBroadcastMessage(AccountingBlock block, String pubKey, byte[] signature) {
this(block, pubKey, signature, Version.getP2PMessageVersion());
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private NewAccountingBlockBroadcastMessage(AccountingBlock block,
String pubKey,
byte[] signature,
int messageVersion) {
super(messageVersion);
this.block = block;
this.pubKey = pubKey;
this.signature = signature;
}
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
return getNetworkEnvelopeBuilder()
.setNewAccountingBlockBroadcastMessage(protobuf.NewAccountingBlockBroadcastMessage.newBuilder()
.setBlock(block.toProtoMessage())
.setPubKey(pubKey)
.setSignature(ByteString.copyFrom(signature))
)
.build();
}
public static NetworkEnvelope fromProto(protobuf.NewAccountingBlockBroadcastMessage proto, int messageVersion) {
return new NewAccountingBlockBroadcastMessage(AccountingBlock.fromProto(proto.getBlock()),
proto.getPubKey(),
proto.getSignature().toByteArray(),
messageVersion);
}
}

View file

@ -0,0 +1,57 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.storage;
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
import bisq.common.proto.persistable.PersistableEnvelope;
import com.google.protobuf.Message;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Getter
public class BurningManAccountingStore implements PersistableEnvelope {
private final LinkedList<AccountingBlock> blocks = new LinkedList<>();
public BurningManAccountingStore(List<AccountingBlock> blocks) {
this.blocks.addAll(blocks);
}
public Message toProtoMessage() {
return protobuf.PersistableEnvelope.newBuilder()
.setBurningManAccountingStore(protobuf.BurningManAccountingStore.newBuilder()
.addAllBlocks(blocks.stream()
.map(AccountingBlock::toProtoMessage)
.collect(Collectors.toList())))
.build();
}
public static BurningManAccountingStore fromProto(protobuf.BurningManAccountingStore proto) {
return new BurningManAccountingStore(proto.getBlocksList().stream()
.map(AccountingBlock::fromProto)
.collect(Collectors.toList()));
}
}

View file

@ -0,0 +1,105 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.accounting.storage;
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
import bisq.network.p2p.storage.persistence.ResourceDataStoreService;
import bisq.network.p2p.storage.persistence.StoreService;
import bisq.common.config.Config;
import bisq.common.persistence.PersistenceManager;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Singleton
public class BurningManAccountingStoreService extends StoreService<BurningManAccountingStore> {
private static final String FILE_NAME = "BurningManAccountingStore";
@Inject
public BurningManAccountingStoreService(ResourceDataStoreService resourceDataStoreService,
@Named(Config.STORAGE_DIR) File storageDir,
PersistenceManager<BurningManAccountingStore> persistenceManager) {
super(storageDir, persistenceManager);
resourceDataStoreService.addService(this);
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void requestPersistence() {
persistenceManager.requestPersistence();
}
public List<AccountingBlock> getBlocks() {
return Collections.unmodifiableList(store.getBlocks());
}
public void addBlock(AccountingBlock block) {
store.getBlocks().add(block);
requestPersistence();
}
public void purgeLastTenBlocks() {
List<AccountingBlock> blocks = store.getBlocks();
if (blocks.size() <= 10) {
blocks.clear();
requestPersistence();
return;
}
List<AccountingBlock> purged = new ArrayList<>(blocks.subList(0, blocks.size() - 10));
blocks.clear();
blocks.addAll(purged);
requestPersistence();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Protected
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected BurningManAccountingStore createStore() {
return new BurningManAccountingStore(new ArrayList<>());
}
@Override
protected void initializePersistenceManager() {
persistenceManager.initialize(store, PersistenceManager.Source.NETWORK);
}
@Override
public String getFileName() {
return FILE_NAME;
}
}

View file

@ -0,0 +1,61 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.model;
import java.util.Date;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@Getter
@EqualsAndHashCode
public final class BurnOutputModel {
private final long amount;
private final long decayedAmount;
private final int height;
private final String txId;
private final long date;
private final int cycleIndex;
public BurnOutputModel(long amount,
long decayedAmount,
int height,
String txId,
long date,
int cycleIndex) {
this.amount = amount;
this.decayedAmount = decayedAmount;
this.height = height;
this.txId = txId;
this.date = date;
this.cycleIndex = cycleIndex;
}
@Override
public String toString() {
return "\n BurnOutputModel{" +
"\r\n amount=" + amount +
",\r\n decayedAmount=" + decayedAmount +
",\r\n height=" + height +
",\r\n txId='" + txId + '\'' +
",\r\n date=" + new Date(date) +
",\r\n cycleIndex=" + cycleIndex +
"\r\n }";
}
}

View file

@ -0,0 +1,117 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.model;
import bisq.core.dao.burningman.BurningManService;
import bisq.common.util.DateUtil;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* Contains all relevant data for a burningman candidate (any contributor who has made a compensation request or was
* a receiver of a genesis output).
*/
@Slf4j
@Getter
@EqualsAndHashCode
public class BurningManCandidate {
private final Set<CompensationModel> compensationModels = new HashSet<>();
private long accumulatedCompensationAmount;
private long accumulatedDecayedCompensationAmount;
private double compensationShare; // Share of accumulated decayed compensation amounts in relation to total issued amounts
protected Optional<String> mostRecentAddress = Optional.empty();
private final Set<BurnOutputModel> burnOutputModels = new HashSet<>();
private final Map<Date, Set<BurnOutputModel>> 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
public BurningManCandidate() {
}
public void addBurnOutputModel(BurnOutputModel burnOutputModel) {
if (burnOutputModels.contains(burnOutputModel)) {
return;
}
burnOutputModels.add(burnOutputModel);
Date month = DateUtil.getStartOfMonth(new Date(burnOutputModel.getDate()));
burnOutputModelsByMonth.putIfAbsent(month, new HashSet<>());
burnOutputModelsByMonth.get(month).add(burnOutputModel);
accumulatedDecayedBurnAmount += burnOutputModel.getDecayedAmount();
accumulatedBurnAmount += burnOutputModel.getAmount();
}
public void addCompensationModel(CompensationModel compensationModel) {
if (compensationModels.contains(compensationModel)) {
return;
}
compensationModels.add(compensationModel);
accumulatedDecayedCompensationAmount += compensationModel.getDecayedAmount();
accumulatedCompensationAmount += compensationModel.getAmount();
mostRecentAddress = compensationModels.stream()
.max(Comparator.comparing(CompensationModel::getHeight))
.map(CompensationModel::getAddress);
}
public Set<String> getAllAddresses() {
return compensationModels.stream().map(CompensationModel::getAddress).collect(Collectors.toSet());
}
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);
}
@Override
public String toString() {
return "BurningManCandidate{" +
"\r\n compensationModels=" + compensationModels +
",\r\n accumulatedCompensationAmount=" + accumulatedCompensationAmount +
",\r\n accumulatedDecayedCompensationAmount=" + accumulatedDecayedCompensationAmount +
",\r\n compensationShare=" + compensationShare +
",\r\n mostRecentAddress=" + mostRecentAddress +
",\r\n burnOutputModels=" + burnOutputModels +
",\r\n accumulatedBurnAmount=" + accumulatedBurnAmount +
",\r\n accumulatedDecayedBurnAmount=" + accumulatedDecayedBurnAmount +
",\r\n burnAmountShare=" + burnAmountShare +
",\r\n cappedBurnAmountShare=" + cappedBurnAmountShare +
"\r\n}";
}
}

View file

@ -0,0 +1,104 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.model;
import java.util.Date;
import java.util.Optional;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@Getter
@EqualsAndHashCode
public final class CompensationModel {
public static CompensationModel fromCompensationRequest(String address,
long amount,
long decayedAmount,
int height,
String txId,
long date,
int cycleIndex) {
return new CompensationModel(address,
amount,
decayedAmount,
height,
txId,
Optional.empty(),
date,
cycleIndex);
}
public static CompensationModel fromGenesisOutput(String address,
long amount,
long decayedAmount,
int height,
String txId,
int outputIndex,
long date) {
return new CompensationModel(address,
amount,
decayedAmount,
height,
txId,
Optional.of(outputIndex),
date,
0);
}
private final String address;
private final long amount;
private final long decayedAmount;
private final int height;
private final String txId;
private final Optional<Integer> outputIndex; // Only set for genesis tx outputs as needed for sorting
private final long date;
private final int cycleIndex;
private CompensationModel(String address,
long amount,
long decayedAmount,
int height,
String txId,
Optional<Integer> outputIndex,
long date,
int cycleIndex) {
this.address = address;
this.amount = amount;
this.decayedAmount = decayedAmount;
this.height = height;
this.txId = txId;
this.outputIndex = outputIndex;
this.date = date;
this.cycleIndex = cycleIndex;
}
@Override
public String toString() {
return "\n CompensationModel{" +
"\r\n address='" + address + '\'' +
",\r\n amount=" + amount +
",\r\n decayedAmount=" + decayedAmount +
",\r\n height=" + height +
",\r\n txId='" + txId + '\'' +
",\r\n outputIndex=" + outputIndex +
",\r\n date=" + new Date(date) +
",\r\n cycleIndex=" + cycleIndex +
"\r\n }";
}
}

View file

@ -0,0 +1,50 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.model;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Getter
@EqualsAndHashCode(callSuper = true)
public final class LegacyBurningMan extends BurningManCandidate {
public LegacyBurningMan(String address) {
mostRecentAddress = Optional.of(address);
}
public void applyBurnAmountShare(double burnAmountShare) {
this.burnAmountShare = burnAmountShare;
this.cappedBurnAmountShare = burnAmountShare;
}
@Override
public void calculateShares(double totalDecayedCompensationAmounts, double totalDecayedBurnAmounts) {
// do nothing
}
@Override
public Set<String> getAllAddresses() {
return mostRecentAddress.map(Set::of).orElse(new HashSet<>());
}
}

View file

@ -0,0 +1,56 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman.model;
import java.util.Date;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@Getter
@EqualsAndHashCode
public final class ReimbursementModel {
private final long amount;
private final int height;
private final long date;
private final int cycleIndex;
private final String txId;
public ReimbursementModel(long amount,
int height,
long date,
int cycleIndex,
String txId) {
this.amount = amount;
this.height = height;
this.date = date;
this.cycleIndex = cycleIndex;
this.txId = txId;
}
@Override
public String toString() {
return "\n ReimbursementModel{" +
",\r\n amount=" + amount +
",\r\n height=" + height +
",\r\n date=" + new Date(date) +
",\r\n cycleIndex=" + cycleIndex +
",\r\n txId=" + txId +
"\r\n }";
}
}

View file

@ -117,8 +117,14 @@ public enum Param {
// Min required trade volume to not get de-listed. Check starts after trial period and use trial period afterwards to look back for trade activity.
ASSET_MIN_VOLUME("0.01", ParamType.BTC, 10, 10),
// LOCK_TIME_TRADE_PAYOUT was never used.
// We re-purpose it as value for BTC fee revenue per cycle. This can be added as oracle data by DAO voting.
// We cannot change the ParamType as that would break consensus
LOCK_TIME_TRADE_PAYOUT("4320", ParamType.BLOCK), // 30 days, can be disabled by setting to 0
// Not used
ARBITRATOR_FEE("0", ParamType.PERCENT), // % of trade. For new trade protocol. Arbitration will become optional and we can apply a fee to it. Initially we start with no fee.
MAX_TRADE_LIMIT("2", ParamType.BTC, 2, 2), // max trade limit for lowest risk payment method. Others will get derived from that.
// The base factor to multiply the bonded role amount. E.g. If Twitter admin has 20 as amount and BONDED_ROLE_FACTOR is 1000 we get 20000 BSQ as required bond.

View file

@ -40,6 +40,8 @@ import org.bitcoinj.core.Transaction;
import javax.inject.Inject;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
@ -48,9 +50,9 @@ import lombok.extern.slf4j.Slf4j;
*/
@Slf4j
public class CompensationProposalFactory extends BaseProposalFactory<CompensationProposal> {
private Coin requestedBsq;
private String bsqAddress;
private Optional<String> burningManReceiverAddress;
@Inject
public CompensationProposalFactory(BsqWalletService bsqWalletService,
@ -65,9 +67,11 @@ public class CompensationProposalFactory extends BaseProposalFactory<Compensatio
public ProposalWithTransaction createProposalWithTransaction(String name,
String link,
Coin requestedBsq)
Coin requestedBsq,
Optional<String> burningManReceiverAddress)
throws ProposalValidationException, InsufficientMoneyException, TxException {
this.requestedBsq = requestedBsq;
this.burningManReceiverAddress = burningManReceiverAddress;
this.bsqAddress = bsqWalletService.getUnusedBsqAddressAsString();
return super.createProposalWithTransaction(name, link);
@ -75,12 +79,17 @@ public class CompensationProposalFactory extends BaseProposalFactory<Compensatio
@Override
protected CompensationProposal createProposalWithoutTxId() {
Map<String, String> extraDataMap = null;
if (burningManReceiverAddress.isPresent()) {
extraDataMap = new HashMap<>();
extraDataMap.put(CompensationProposal.BURNING_MAN_RECEIVER_ADDRESS, burningManReceiverAddress.get());
}
return new CompensationProposal(
name,
link,
requestedBsq,
bsqAddress,
new HashMap<>());
extraDataMap);
}
@Override

View file

@ -17,7 +17,7 @@
package bisq.core.dao.node.full;
class RpcException extends Exception {
public class RpcException extends Exception {
RpcException(String message, Throwable cause) {
super(message, cause);
}

View file

@ -56,6 +56,8 @@ import java.io.IOException;
import java.math.BigDecimal;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@ -91,6 +93,14 @@ public class RpcService {
// Keep that for optimization after measuring performance differences
private final ListeningExecutorService executor = Utilities.getSingleThreadListeningExecutor("RpcService");
private volatile boolean isShutDown;
private final Set<ResultHandler> setupResultHandlers = new CopyOnWriteArraySet<>();
private final Set<Consumer<Throwable>> setupErrorHandlers = new CopyOnWriteArraySet<>();
private volatile boolean setupComplete;
private Consumer<RawDtoBlock> rawDtoBlockHandler;
private Consumer<Throwable> rawDtoBlockErrorHandler;
private Consumer<RawBlock> rawBlockHandler;
private Consumer<Throwable> rawBlockErrorHandler;
private volatile boolean isBlockHandlerSet;
///////////////////////////////////////////////////////////////////////////////////////////
@ -139,7 +149,20 @@ public class RpcService {
executor.shutdown();
}
void setup(ResultHandler resultHandler, Consumer<Throwable> errorHandler) {
public void setup(ResultHandler resultHandler, Consumer<Throwable> errorHandler) {
if (setupComplete) {
// Setup got already called and has finished.
resultHandler.handleResult();
return;
} else {
setupResultHandlers.add(resultHandler);
setupErrorHandlers.add(errorHandler);
if (setupResultHandlers.size() > 1) {
// Setup got already called but has not finished yet.
return;
}
}
try {
ListenableFuture<Void> future = executor.submit(() -> {
try {
@ -159,7 +182,11 @@ public class RpcService {
daemon = new BitcoindDaemon(rpcBlockHost, rpcBlockPort, throwable -> {
log.error(throwable.toString());
throwable.printStackTrace();
UserThread.execute(() -> errorHandler.accept(new RpcException(throwable)));
UserThread.execute(() -> {
setupErrorHandlers.forEach(handler -> handler.accept(new RpcException(throwable)));
setupErrorHandlers.clear();
setupResultHandlers.clear();
});
});
log.info("Setup took {} ms", System.currentTimeMillis() - startTs);
@ -173,11 +200,20 @@ public class RpcService {
Futures.addCallback(future, new FutureCallback<>() {
public void onSuccess(Void ignore) {
UserThread.execute(resultHandler::handleResult);
setupComplete = true;
UserThread.execute(() -> {
setupResultHandlers.forEach(ResultHandler::handleResult);
setupResultHandlers.clear();
setupErrorHandlers.clear();
});
}
public void onFailure(@NotNull Throwable throwable) {
UserThread.execute(() -> errorHandler.accept(throwable));
UserThread.execute(() -> {
setupErrorHandlers.forEach(handler -> handler.accept(throwable));
setupErrorHandlers.clear();
setupResultHandlers.clear();
});
}
}, MoreExecutors.directExecutor());
} catch (Exception e) {
@ -219,21 +255,49 @@ public class RpcService {
void addNewDtoBlockHandler(Consumer<RawBlock> dtoBlockHandler,
Consumer<Throwable> errorHandler) {
this.rawBlockHandler = dtoBlockHandler;
this.rawBlockErrorHandler = errorHandler;
if (!isBlockHandlerSet) {
setupBlockHandler();
}
}
public void addNewRawDtoBlockHandler(Consumer<RawDtoBlock> rawDtoBlockHandler,
Consumer<Throwable> errorHandler) {
this.rawDtoBlockHandler = rawDtoBlockHandler;
this.rawDtoBlockErrorHandler = errorHandler;
if (!isBlockHandlerSet) {
setupBlockHandler();
}
}
private void setupBlockHandler() {
isBlockHandlerSet = true;
daemon.setBlockListener(blockHash -> {
try {
var rawDtoBlock = client.getBlock(blockHash, 2);
log.info("New block received: height={}, id={}", rawDtoBlock.getHeight(), rawDtoBlock.getHash());
RawDtoBlock rawDtoBlock = client.getBlock(blockHash, 2);
log.info("New rawDtoBlock received: height={}, hash={}", rawDtoBlock.getHeight(), rawDtoBlock.getHash());
var block = getBlockFromRawDtoBlock(rawDtoBlock);
UserThread.execute(() -> dtoBlockHandler.accept(block));
if (rawBlockHandler != null) {
RawBlock rawBlock = getRawBlockFromRawDtoBlock(rawDtoBlock);
UserThread.execute(() -> rawBlockHandler.accept(rawBlock));
}
if (rawDtoBlockHandler != null) {
UserThread.execute(() -> rawDtoBlockHandler.accept(rawDtoBlock));
}
} catch (Throwable throwable) {
log.error("Error at BlockHandler", throwable);
errorHandler.accept(throwable);
if (rawBlockErrorHandler != null) {
rawBlockErrorHandler.accept(throwable);
}
if (rawDtoBlockErrorHandler != null) {
rawDtoBlockErrorHandler.accept(throwable);
}
}
});
}
void requestChainHeadHeight(Consumer<Integer> resultHandler, Consumer<Throwable> errorHandler) {
public void requestChainHeadHeight(Consumer<Integer> resultHandler, Consumer<Throwable> errorHandler) {
try {
ListenableFuture<Integer> future = executor.submit(client::getBlockCount);
Futures.addCallback(future, new FutureCallback<>() {
@ -262,7 +326,7 @@ public class RpcService {
long startTs = System.currentTimeMillis();
String blockHash = client.getBlockHash(blockHeight);
var rawDtoBlock = client.getBlock(blockHash, 2);
var block = getBlockFromRawDtoBlock(rawDtoBlock);
var block = getRawBlockFromRawDtoBlock(rawDtoBlock);
log.info("requestDtoBlock from bitcoind at blockHeight {} with {} txs took {} ms",
blockHeight, block.getRawTxs().size(), System.currentTimeMillis() - startTs);
return block;
@ -289,12 +353,47 @@ public class RpcService {
}
}
public void requestRawDtoBlock(int blockHeight,
Consumer<RawDtoBlock> rawDtoBlockHandler,
Consumer<Throwable> errorHandler) {
try {
ListenableFuture<RawDtoBlock> future = executor.submit(() -> {
long startTs = System.currentTimeMillis();
String blockHash = client.getBlockHash(blockHeight);
// getBlock with about 2000 tx takes about 500 ms on a 4GHz Intel Core i7 CPU.
var rawDtoBlock = client.getBlock(blockHash, 2);
log.info("requestRawDtoBlock from bitcoind at blockHeight {} with {} txs took {} ms",
blockHeight, rawDtoBlock.getTx().size(), System.currentTimeMillis() - startTs);
return rawDtoBlock;
});
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(RawDtoBlock rawDtoBlock) {
UserThread.execute(() -> rawDtoBlockHandler.accept(rawDtoBlock));
}
@Override
public void onFailure(@NotNull Throwable throwable) {
log.error("Error at requestRawDtoBlock: blockHeight={}", blockHeight);
UserThread.execute(() -> errorHandler.accept(throwable));
}
}, MoreExecutors.directExecutor());
} catch (Exception e) {
if (!isShutDown || !(e instanceof RejectedExecutionException)) {
log.warn(e.toString(), e);
throw e;
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private static RawBlock getBlockFromRawDtoBlock(RawDtoBlock rawDtoBlock) {
private static RawBlock getRawBlockFromRawDtoBlock(RawDtoBlock rawDtoBlock) {
List<RawTx> txList = rawDtoBlock.getTx().stream()
.map(e -> getTxFromRawTransaction(e, rawDtoBlock))
.collect(Collectors.toList());

View file

@ -201,6 +201,32 @@ public class DaoStateService implements DaoSetupService {
return getCycle(blockHeight).map(cycle -> cycle.getHeightOfFirstBlock());
}
public Optional<Cycle> getNextCycle(Cycle cycle) {
return getCycle(cycle.getHeightOfLastBlock() + 1);
}
public Optional<Cycle> getPreviousCycle(Cycle cycle) {
return getCycle(cycle.getHeightOfFirstBlock() - 1);
}
public Optional<Cycle> getPastCycle(Cycle cycle, int numPastCycles) {
Optional<Cycle> previous = Optional.empty();
Cycle current = cycle;
for (int i = 0; i < numPastCycles; i++) {
previous = getPreviousCycle(current);
if (previous.isPresent()) {
current = previous.get();
} else {
break;
}
}
return previous;
}
public Cycle getCycleAtIndex(int index) {
return getCycles().get(index);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Block
@ -440,7 +466,7 @@ public class DaoStateService implements DaoSetupService {
// TxOutput
///////////////////////////////////////////////////////////////////////////////////////////
private Stream<TxOutput> getUnorderedTxOutputStream() {
public Stream<TxOutput> getUnorderedTxOutputStream() {
return getUnorderedTxStream()
.flatMap(tx -> tx.getTxOutputs().stream());
}

View file

@ -30,11 +30,13 @@ import org.bitcoinj.core.Coin;
import java.util.Date;
import java.util.Map;
import java.util.Optional;
import lombok.EqualsAndHashCode;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
@Immutable
@ -42,6 +44,9 @@ import javax.annotation.concurrent.Immutable;
@EqualsAndHashCode(callSuper = true)
@Value
public final class CompensationProposal extends Proposal implements IssuanceProposal, ImmutableDaoStateModel {
// Keys for extra map
public static final String BURNING_MAN_RECEIVER_ADDRESS = "burningManReceiverAddress";
private final long requestedBsq;
private final String bsqAddress;
@ -49,7 +54,7 @@ public final class CompensationProposal extends Proposal implements IssuanceProp
String link,
Coin requestedBsq,
String bsqAddress,
Map<String, String> extraDataMap) {
@Nullable Map<String, String> extraDataMap) {
this(name,
link,
bsqAddress,
@ -72,7 +77,7 @@ public final class CompensationProposal extends Proposal implements IssuanceProp
byte version,
long creationDate,
String txId,
Map<String, String> extraDataMap) {
@Nullable Map<String, String> extraDataMap) {
super(name,
link,
version,
@ -135,6 +140,11 @@ public final class CompensationProposal extends Proposal implements IssuanceProp
return TxType.COMPENSATION_REQUEST;
}
// Added at v.1.9.7
public Optional<String> getBurningManReceiverAddress() {
return Optional.ofNullable(extraDataMap).flatMap(map -> Optional.ofNullable(map.get(BURNING_MAN_RECEIVER_ADDRESS)));
}
@Override
public Proposal cloneProposalAndAddTxId(String txId) {
return new CompensationProposal(getName(),
@ -152,6 +162,7 @@ public final class CompensationProposal extends Proposal implements IssuanceProp
return "CompensationProposal{" +
"\n requestedBsq=" + requestedBsq +
",\n bsqAddress='" + bsqAddress + '\'' +
",\n burningManReceiverAddress='" + getBurningManReceiverAddress() + '\'' +
"\n} " + super.toString();
}
}

View file

@ -145,7 +145,7 @@ public abstract class Proposal implements PersistablePayload, NetworkPayload, Co
"\n txId='" + txId + '\'' +
",\n name='" + name + '\'' +
",\n link='" + link + '\'' +
",\n txId='" + txId + '\'' +
",\n extraDataMap='" + extraDataMap + '\'' +
",\n version=" + version +
",\n creationDate=" + new Date(creationDate) +
"\n}";

View file

@ -22,6 +22,9 @@ import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.dao.DaoFacade;
import bisq.core.dao.burningman.BtcFeeReceiverService;
import bisq.core.dao.burningman.BurningManService;
import bisq.core.dao.burningman.DelayedPayoutTxReceiverService;
import bisq.core.exceptions.TradePriceOutOfToleranceException;
import bisq.core.filter.FilterManager;
import bisq.core.locale.Res;
@ -122,6 +125,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
private final RefundAgentManager refundAgentManager;
private final DaoFacade daoFacade;
private final FilterManager filterManager;
private final BtcFeeReceiverService btcFeeReceiverService;
private final DelayedPayoutTxReceiverService delayedPayoutTxReceiverService;
private final Broadcaster broadcaster;
private final PersistenceManager<TradableList<OpenOffer>> persistenceManager;
private final Map<String, OpenOffer> offersToBeEdited = new HashMap<>();
@ -155,6 +160,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
RefundAgentManager refundAgentManager,
DaoFacade daoFacade,
FilterManager filterManager,
BtcFeeReceiverService btcFeeReceiverService,
DelayedPayoutTxReceiverService delayedPayoutTxReceiverService,
Broadcaster broadcaster,
PersistenceManager<TradableList<OpenOffer>> persistenceManager) {
this.coreContext = coreContext;
@ -175,6 +182,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
this.refundAgentManager = refundAgentManager;
this.daoFacade = daoFacade;
this.filterManager = filterManager;
this.btcFeeReceiverService = btcFeeReceiverService;
this.delayedPayoutTxReceiverService = delayedPayoutTxReceiverService;
this.broadcaster = broadcaster;
this.persistenceManager = persistenceManager;
@ -389,6 +398,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
arbitratorManager,
tradeStatisticsManager,
daoFacade,
btcFeeReceiverService,
user,
filterManager);
PlaceOfferProtocol placeOfferProtocol = new PlaceOfferProtocol(
@ -663,6 +673,22 @@ 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<OpenOffer> openOfferOptional = getOpenOfferById(request.offerId);
AvailabilityResult availabilityResult;

View file

@ -17,6 +17,7 @@
package bisq.core.offer.availability;
import bisq.core.dao.burningman.DelayedPayoutTxReceiverService;
import bisq.core.offer.Offer;
import bisq.core.offer.availability.messages.OfferAvailabilityResponse;
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
@ -70,12 +71,17 @@ public class OfferAvailabilityModel implements Model {
@Getter
private final boolean isTakerApiUser;
// Added in v 1.9.7
@Getter
private final DelayedPayoutTxReceiverService delayedPayoutTxReceiverService;
public OfferAvailabilityModel(Offer offer,
PubKeyRing pubKeyRing,
P2PService p2PService,
User user,
MediatorManager mediatorManager,
TradeStatisticsManager tradeStatisticsManager,
DelayedPayoutTxReceiverService delayedPayoutTxReceiverService,
boolean isTakerApiUser) {
this.offer = offer;
this.pubKeyRing = pubKeyRing;
@ -83,6 +89,7 @@ public class OfferAvailabilityModel implements Model {
this.user = user;
this.mediatorManager = mediatorManager;
this.tradeStatisticsManager = tradeStatisticsManager;
this.delayedPayoutTxReceiverService = delayedPayoutTxReceiverService;
this.isTakerApiUser = isTakerApiUser;
}

View file

@ -44,17 +44,23 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp
private final Capabilities supportedCapabilities;
private final boolean isTakerApiUser;
// Added in v 1.9.7
private final int burningManSelectionHeight;
public OfferAvailabilityRequest(String offerId,
PubKeyRing pubKeyRing,
long takersTradePrice,
boolean isTakerApiUser) {
boolean isTakerApiUser,
int burningManSelectionHeight) {
this(offerId,
pubKeyRing,
takersTradePrice,
isTakerApiUser,
burningManSelectionHeight,
Capabilities.app,
Version.getP2PMessageVersion(),
UUID.randomUUID().toString());
UUID.randomUUID().toString()
);
}
@ -66,6 +72,7 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp
PubKeyRing pubKeyRing,
long takersTradePrice,
boolean isTakerApiUser,
int burningManSelectionHeight,
@Nullable Capabilities supportedCapabilities,
int messageVersion,
@Nullable String uid) {
@ -73,6 +80,7 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp
this.pubKeyRing = pubKeyRing;
this.takersTradePrice = takersTradePrice;
this.isTakerApiUser = isTakerApiUser;
this.burningManSelectionHeight = burningManSelectionHeight;
this.supportedCapabilities = supportedCapabilities;
}
@ -82,7 +90,8 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp
.setOfferId(offerId)
.setPubKeyRing(pubKeyRing.toProtoMessage())
.setTakersTradePrice(takersTradePrice)
.setIsTakerApiUser(isTakerApiUser);
.setIsTakerApiUser(isTakerApiUser)
.setBurningManSelectionHeight(burningManSelectionHeight);
Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities)));
Optional.ofNullable(uid).ifPresent(e -> builder.setUid(uid));
@ -97,6 +106,7 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp
PubKeyRing.fromProto(proto.getPubKeyRing()),
proto.getTakersTradePrice(),
proto.getIsTakerApiUser(),
proto.getBurningManSelectionHeight(),
Capabilities.fromIntList(proto.getSupportedCapabilitiesList()),
messageVersion,
proto.getUid().isEmpty() ? null : proto.getUid());

View file

@ -39,8 +39,12 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
try {
runInterceptHook();
int burningManSelectionHeight = model.getDelayedPayoutTxReceiverService().getBurningManSelectionHeight();
OfferAvailabilityRequest message = new OfferAvailabilityRequest(model.getOffer().getId(),
model.getPubKeyRing(), model.getTakersTradePrice(), model.isTakerApiUser());
model.getPubKeyRing(),
model.getTakersTradePrice(),
model.isTakerApiUser(),
burningManSelectionHeight);
log.info("Send {} with offerId {} and uid {} to peer {}",
message.getClass().getSimpleName(), message.getOfferId(),
message.getUid(), model.getPeerNodeAddress());

View file

@ -21,6 +21,7 @@ import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.dao.DaoFacade;
import bisq.core.dao.burningman.BtcFeeReceiverService;
import bisq.core.filter.FilterManager;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferBookService;
@ -51,6 +52,7 @@ public class PlaceOfferModel implements Model {
private final ArbitratorManager arbitratorManager;
private final TradeStatisticsManager tradeStatisticsManager;
private final DaoFacade daoFacade;
private final BtcFeeReceiverService btcFeeReceiverService;
private final User user;
@Getter
private final FilterManager filterManager;
@ -71,6 +73,7 @@ public class PlaceOfferModel implements Model {
ArbitratorManager arbitratorManager,
TradeStatisticsManager tradeStatisticsManager,
DaoFacade daoFacade,
BtcFeeReceiverService btcFeeReceiverService,
User user,
FilterManager filterManager) {
this.offer = offer;
@ -83,6 +86,7 @@ public class PlaceOfferModel implements Model {
this.arbitratorManager = arbitratorManager;
this.tradeStatisticsManager = tradeStatisticsManager;
this.daoFacade = daoFacade;
this.btcFeeReceiverService = btcFeeReceiverService;
this.user = user;
this.filterManager = filterManager;
}

View file

@ -28,7 +28,6 @@ import bisq.core.dao.exceptions.DaoDisabledException;
import bisq.core.dao.state.model.blockchain.TxType;
import bisq.core.offer.Offer;
import bisq.core.offer.placeoffer.bisq_v1.PlaceOfferModel;
import bisq.core.util.FeeReceiverSelector;
import bisq.common.UserThread;
import bisq.common.taskrunner.Task;
@ -66,9 +65,8 @@ public class CreateMakerFeeTx extends Task<PlaceOfferModel> {
TradeWalletService tradeWalletService = model.getTradeWalletService();
String feeReceiver = FeeReceiverSelector.getAddress(model.getFilterManager());
if (offer.isCurrencyForMakerFeeBtc()) {
String feeReceiver = model.getBtcFeeReceiverService().getAddress();
tradeWalletService.createBtcTradingFeeTx(
fundingAddress,
reservedForTradeAddress,

View file

@ -19,6 +19,9 @@ package bisq.core.proto.network;
import bisq.core.alert.Alert;
import bisq.core.alert.PrivateNotificationMessage;
import bisq.core.dao.burningman.accounting.node.messages.GetAccountingBlocksRequest;
import bisq.core.dao.burningman.accounting.node.messages.GetAccountingBlocksResponse;
import bisq.core.dao.burningman.accounting.node.messages.NewAccountingBlockBroadcastMessage;
import bisq.core.dao.governance.blindvote.network.messages.RepublishGovernanceDataRequest;
import bisq.core.dao.governance.proposal.storage.temp.TempProposalPayload;
import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesRequest;
@ -254,6 +257,13 @@ public class CoreNetworkProtoResolver extends CoreProtoResolver implements Netwo
case GET_INVENTORY_RESPONSE:
return GetInventoryResponse.fromProto(proto.getGetInventoryResponse(), messageVersion);
case GET_ACCOUNTING_BLOCKS_REQUEST:
return GetAccountingBlocksRequest.fromProto(proto.getGetAccountingBlocksRequest(), messageVersion);
case GET_ACCOUNTING_BLOCKS_RESPONSE:
return GetAccountingBlocksResponse.fromProto(proto.getGetAccountingBlocksResponse(), messageVersion);
case NEW_ACCOUNTING_BLOCK_BROADCAST_MESSAGE:
return NewAccountingBlockBroadcastMessage.fromProto(proto.getNewAccountingBlockBroadcastMessage(), messageVersion);
default:
throw new ProtobufferException("Unknown proto message case (PB.NetworkEnvelope). messageCase=" +
proto.getMessageCase() + "; proto raw data=" + proto.toString());

View file

@ -21,6 +21,7 @@ import bisq.core.account.sign.SignedWitnessStore;
import bisq.core.account.witness.AccountAgeWitnessStore;
import bisq.core.btc.model.AddressEntryList;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.dao.burningman.accounting.storage.BurningManAccountingStore;
import bisq.core.dao.governance.blindvote.MyBlindVoteList;
import bisq.core.dao.governance.blindvote.storage.BlindVoteStore;
import bisq.core.dao.governance.bond.reputation.MyReputationList;
@ -141,6 +142,8 @@ public class CorePersistenceProtoResolver extends CoreProtoResolver implements P
return RemovedPayloadsMap.fromProto(proto.getRemovedPayloadsMap());
case BSQ_BLOCK_STORE:
return BsqBlockStore.fromProto(proto.getBsqBlockStore());
case BURNING_MAN_ACCOUNTING_STORE:
return BurningManAccountingStore.fromProto(proto.getBurningManAccountingStore());
default:
throw new ProtobufferRuntimeException("Unknown proto message case(PB.PersistableEnvelope). " +
"messageCase=" + proto.getMessageCase() + "; proto raw data=" + proto.toString());

View file

@ -77,7 +77,6 @@ import javax.annotation.Nullable;
@EqualsAndHashCode
@Getter
public final class Dispute implements NetworkPayload, PersistablePayload {
public enum State {
NEEDS_UPGRADE,
NEW,
@ -146,6 +145,13 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
// Added at v1.6.0
private Dispute.State disputeState = State.NEW;
// Added in v 1.9.7
@Setter
private int burningManSelectionHeight;
@Setter
private long tradeTxFee;
// Should be only used in emergency case if we need to add data but do not want to break backward compatibility
// at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new
// field in a class would break that hash and therefore break the storage mechanism.
@ -263,7 +269,9 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
.setIsClosed(this.isClosed())
.setOpeningDate(openingDate)
.setState(Dispute.State.toProtoMessage(disputeState))
.setId(id);
.setId(id)
.setBurningManSelectionHeight(burningManSelectionHeight)
.setTradeTxFee(tradeTxFee);
Optional.ofNullable(contractHash).ifPresent(e -> builder.setContractHash(ByteString.copyFrom(e)));
Optional.ofNullable(depositTxSerialized).ifPresent(e -> builder.setDepositTxSerialized(ByteString.copyFrom(e)));
@ -330,6 +338,9 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
dispute.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx);
}
dispute.setBurningManSelectionHeight(proto.getBurningManSelectionHeight());
dispute.setTradeTxFee(proto.getTradeTxFee());
if (Dispute.State.fromProto(proto.getState()) == State.NEEDS_UPGRADE) {
// old disputes did not have a state field, so choose an appropriate state:
dispute.setState(proto.getIsClosed() ? State.CLOSED : State.OPEN);
@ -516,6 +527,13 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
return cachedDepositTx;
}
// Dispute agents might receive disputes created before activation date.
// By checking if burningManSelectionHeight is > 0 we can detect if the trade was created with
// the new burningmen receivers or with legacy BM.
public boolean isUsingLegacyBurningMan() {
return burningManSelectionHeight == 0;
}
@Override
public String toString() {
return "Dispute{" +
@ -550,6 +568,8 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
",\n delayedPayoutTxId='" + delayedPayoutTxId + '\'' +
",\n donationAddressOfDelayedPayoutTx='" + donationAddressOfDelayedPayoutTx + '\'' +
",\n cachedDepositTx='" + cachedDepositTx + '\'' +
",\n burningManSelectionHeight='" + burningManSelectionHeight + '\'' +
",\n tradeTxFee='" + tradeTxFee + '\'' +
"\n}";
}
}

View file

@ -273,8 +273,10 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
List<Dispute> disputes = getDisputeList().getList();
disputes.forEach(dispute -> {
try {
DisputeValidation.validateDonationAddressMatchesAnyPastParamValues(dispute, dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade);
DisputeValidation.validateNodeAddresses(dispute, config);
if (dispute.isUsingLegacyBurningMan()) {
DisputeValidation.validateDonationAddressMatchesAnyPastParamValues(dispute, dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade);
}
} catch (DisputeValidation.AddressException | DisputeValidation.NodeAddressException e) {
log.error(e.toString());
validationExceptions.add(e);
@ -371,8 +373,10 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
DisputeValidation.validateDisputeData(dispute, btcWalletService);
DisputeValidation.validateNodeAddresses(dispute, config);
DisputeValidation.validateSenderNodeAddress(dispute, openNewDisputeMessage.getSenderNodeAddress());
DisputeValidation.validateDonationAddressMatchesAnyPastParamValues(dispute, dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade);
DisputeValidation.testIfDisputeTriesReplay(dispute, disputeList.getList());
if (dispute.isUsingLegacyBurningMan()) {
DisputeValidation.validateDonationAddressMatchesAnyPastParamValues(dispute, dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade);
}
} catch (DisputeValidation.ValidationException e) {
log.error(e.toString());
validationExceptions.add(e);
@ -401,13 +405,15 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
DisputeValidation.validateDisputeData(dispute, btcWalletService);
DisputeValidation.validateNodeAddresses(dispute, config);
DisputeValidation.validateTradeAndDispute(dispute, trade);
DisputeValidation.validateDonationAddress(dispute,
Objects.requireNonNull(trade.getDelayedPayoutTx()),
btcWalletService.getParams(),
daoFacade);
TradeDataValidation.validateDelayedPayoutTx(trade,
trade.getDelayedPayoutTx(),
btcWalletService);
if (dispute.isUsingLegacyBurningMan()) {
DisputeValidation.validateDonationAddress(dispute,
Objects.requireNonNull(trade.getDelayedPayoutTx()),
btcWalletService.getParams());
DisputeValidation.validateDonationAddressMatchesAnyPastParamValues(dispute, dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade);
}
} catch (TradeDataValidation.ValidationException | DisputeValidation.ValidationException e) {
// The peer sent us an invalid donation address. We do not return here as we don't want to break
// mediation/arbitration and log only the issue. The dispute agent will run validation as well and will get
@ -618,6 +624,8 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
dispute.setExtraDataMap(disputeFromOpener.getExtraDataMap());
dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId());
dispute.setDonationAddressOfDelayedPayoutTx(disputeFromOpener.getDonationAddressOfDelayedPayoutTx());
dispute.setBurningManSelectionHeight(disputeFromOpener.getBurningManSelectionHeight());
dispute.setTradeTxFee(disputeFromOpener.getTradeTxFee());
Optional<Dispute> storedDisputeOptional = findDispute(dispute);

View file

@ -115,6 +115,7 @@ public class DisputeValidation {
}
}
public static void validateSenderNodeAddress(Dispute dispute,
NodeAddress senderNodeAddress) throws NodeAddressException {
if (!senderNodeAddress.equals(dispute.getContract().getBuyerNodeAddress())
@ -156,8 +157,7 @@ public class DisputeValidation {
public static void validateDonationAddress(Dispute dispute,
Transaction delayedPayoutTx,
NetworkParameters params,
DaoFacade daoFacade)
NetworkParameters params)
throws AddressException {
TransactionOutput output = delayedPayoutTx.getOutput(0);
Address address = output.getScriptPubKey().getToAddress(params);
@ -167,12 +167,13 @@ public class DisputeValidation {
log.error(delayedPayoutTx.toString());
throw new DisputeValidation.AddressException(dispute, errorMsg);
}
String delayedPayoutTxOutputAddress = address.toString();
validateDonationAddressMatchesAnyPastParamValues(dispute, delayedPayoutTxOutputAddress, daoFacade);
// Verify that address in the dispute matches the one in the trade.
String delayedPayoutTxOutputAddress = address.toString();
checkArgument(delayedPayoutTxOutputAddress.equals(dispute.getDonationAddressOfDelayedPayoutTx()),
"donationAddressOfDelayedPayoutTx from dispute does not match address from delayed payout tx");
"donationAddressOfDelayedPayoutTx from dispute does not match address from delayed payout tx. " +
"delayedPayoutTxOutputAddress=" + delayedPayoutTxOutputAddress +
"; dispute.getDonationAddressOfDelayedPayoutTx()=" + dispute.getDonationAddressOfDelayedPayoutTx());
}
public static void testIfAnyDisputeTriedReplay(List<Dispute> disputeList,
@ -244,7 +245,6 @@ public class DisputeValidation {
Map<String, Set<String>> disputesPerDelayedPayoutTxId,
Map<String, Set<String>> disputesPerDepositTxId)
throws DisputeReplayException {
try {
String disputeToTestTradeId = disputeToTest.getTradeId();
String disputeToTestDelayedPayoutTxId = disputeToTest.getDelayedPayoutTxId();
@ -312,6 +312,7 @@ public class DisputeValidation {
}
}
public static class AddressException extends ValidationException {
AddressException(Dispute dispute, String msg) {
super(dispute, msg);

View file

@ -21,6 +21,7 @@ import bisq.core.btc.setup.WalletsSetup;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.dao.DaoFacade;
import bisq.core.dao.burningman.DelayedPayoutTxReceiverService;
import bisq.core.locale.Res;
import bisq.core.offer.OpenOffer;
import bisq.core.offer.OpenOfferManager;
@ -50,11 +51,13 @@ import bisq.common.app.Version;
import bisq.common.config.Config;
import bisq.common.crypto.KeyRing;
import bisq.common.util.Hex;
import bisq.common.util.Tuple2;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.core.TransactionOutput;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@ -74,8 +77,10 @@ import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
@Singleton
public final class RefundManager extends DisputeManager<RefundDisputeList> {
private final DelayedPayoutTxReceiverService delayedPayoutTxReceiverService;
private final MempoolService mempoolService;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@ -89,6 +94,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
ClosedTradableManager closedTradableManager,
OpenOfferManager openOfferManager,
DaoFacade daoFacade,
DelayedPayoutTxReceiverService delayedPayoutTxReceiverService,
KeyRing keyRing,
RefundDisputeListService refundDisputeListService,
Config config,
@ -96,6 +102,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
MempoolService mempoolService) {
super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager,
openOfferManager, daoFacade, keyRing, refundDisputeListService, config, priceFeedService);
this.delayedPayoutTxReceiverService = delayedPayoutTxReceiverService;
this.mempoolService = mempoolService;
}
@ -308,4 +315,27 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
checkArgument(fundingTxId.equals(depositTx.getTxId().toString()),
"First input at delayedPayoutTx does not connect to depositTx");
}
public void verifyDelayedPayoutTxReceivers(Transaction delayedPayoutTx, Dispute dispute) {
Transaction depositTx = dispute.findDepositTx(btcWalletService).orElseThrow();
long inputAmount = depositTx.getOutput(0).getValue().value;
int selectionHeight = dispute.getBurningManSelectionHeight();
List<Tuple2<Long, String>> delayedPayoutTxReceivers = delayedPayoutTxReceiverService.getReceivers(
selectionHeight,
inputAmount,
dispute.getTradeTxFee());
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");
NetworkParameters params = btcWalletService.getParams();
for (int i = 0; i < delayedPayoutTx.getOutputs().size(); i++) {
TransactionOutput transactionOutput = delayedPayoutTx.getOutputs().get(i);
Tuple2<Long, String> receiverTuple = delayedPayoutTxReceivers.get(0);
checkArgument(transactionOutput.getScriptPubKey().getToAddress(params).toString().equals(receiverTuple.second),
"output address does not match delayedPayoutTxReceivers address. transactionOutput=" + transactionOutput);
checkArgument(transactionOutput.getValue().value == receiverTuple.first,
"output value does not match delayedPayoutTxReceivers value. transactionOutput=" + transactionOutput);
}
}
}

View file

@ -77,6 +77,7 @@ import bisq.network.p2p.P2PService;
import bisq.network.p2p.network.TorNetworkNode;
import bisq.common.ClockWatcher;
import bisq.common.app.DevEnv;
import bisq.common.config.Config;
import bisq.common.crypto.KeyRing;
import bisq.common.handlers.ErrorMessageHandler;
@ -644,6 +645,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
user,
mediatorManager,
tradeStatisticsManager,
provider.getDelayedPayoutTxReceiverService(),
isTakerApiUser);
}
@ -729,6 +731,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
clockWatcher.addListener(new ClockWatcher.Listener() {
@Override
public void onSecondTick() {
if (DevEnv.isDevMode()) {
updateTradePeriodState();
}
}
@Override
@ -743,6 +748,12 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
if (!trade.isPayoutPublished()) {
Date maxTradePeriodDate = trade.getMaxTradePeriodDate();
Date halfTradePeriodDate = trade.getHalfTradePeriodDate();
if (DevEnv.isDevMode()) {
TransactionConfidence confidenceForTxId = btcWalletService.getConfidenceForTxId(trade.getDepositTxId());
if (confidenceForTxId != null && confidenceForTxId.getDepthInBlocks() > 4) {
trade.setTradePeriodState(Trade.TradePeriodState.TRADE_PERIOD_OVER);
}
}
if (maxTradePeriodDate != null && halfTradePeriodDate != null) {
Date now = new Date();
if (now.after(maxTradePeriodDate)) {

View file

@ -71,12 +71,6 @@ public class TradeDataValidation {
throw new InvalidTxException(errorMsg);
}
if (delayedPayoutTx.getOutputs().size() != 1) {
errorMsg = "Number of delayedPayoutTx outputs must be 1";
log.error(errorMsg);
log.error(delayedPayoutTx.toString());
throw new InvalidTxException(errorMsg);
}
// connectedOutput is null and input.getValue() is null at that point as the tx is not committed to the wallet
// yet. So we cannot check that the input matches but we did the amount check earlier in the trade protocol.
@ -97,6 +91,14 @@ public class TradeDataValidation {
throw new InvalidLockTimeException(errorMsg);
}
if (trade.isUsingLegacyBurningMan()) {
if (delayedPayoutTx.getOutputs().size() != 1) {
errorMsg = "Number of delayedPayoutTx outputs must be 1";
log.error(errorMsg);
log.error(delayedPayoutTx.toString());
throw new InvalidTxException(errorMsg);
}
// Check amount
TransactionOutput output = delayedPayoutTx.getOutput(0);
Offer offer = checkNotNull(trade.getOffer());
@ -112,11 +114,12 @@ public class TradeDataValidation {
}
NetworkParameters params = btcWalletService.getParams();
String delayedPayoutTxOutputAddress = output.getScriptPubKey().getToAddress(params).toString();
if (addressConsumer != null) {
String delayedPayoutTxOutputAddress = output.getScriptPubKey().getToAddress(params).toString();
addressConsumer.accept(delayedPayoutTxOutputAddress);
}
}
}
public static void validatePayoutTxInput(Transaction depositTx,
Transaction delayedPayoutTx)

View file

@ -1067,6 +1067,12 @@ public abstract class Trade extends TradeModel {
return offer != null && offer.isBsqSwapOffer();
}
// By checking if burningManSelectionHeight is 0 we can detect if the trade was created with
// the new burningmen receivers or with legacy BM.
public boolean isUsingLegacyBurningMan() {
return processModel.getBurningManSelectionHeight() == 0;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private

View file

@ -23,6 +23,8 @@ import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.btc.wallet.WalletsManager;
import bisq.core.dao.DaoFacade;
import bisq.core.dao.burningman.BtcFeeReceiverService;
import bisq.core.dao.burningman.DelayedPayoutTxReceiverService;
import bisq.core.filter.FilterManager;
import bisq.core.offer.OpenOfferManager;
import bisq.core.provider.fee.FeeService;
@ -60,6 +62,8 @@ public class Provider {
private final RefundAgentManager refundAgentManager;
private final KeyRing keyRing;
private final FeeService feeService;
private final BtcFeeReceiverService btcFeeReceiverService;
private final DelayedPayoutTxReceiverService delayedPayoutTxReceiverService;
@Inject
public Provider(OpenOfferManager openOfferManager,
@ -78,7 +82,9 @@ public class Provider {
MediatorManager mediatorManager,
RefundAgentManager refundAgentManager,
KeyRing keyRing,
FeeService feeService) {
FeeService feeService,
BtcFeeReceiverService btcFeeReceiverService,
DelayedPayoutTxReceiverService delayedPayoutTxReceiverService) {
this.openOfferManager = openOfferManager;
this.p2PService = p2PService;
@ -97,5 +103,7 @@ public class Provider {
this.refundAgentManager = refundAgentManager;
this.keyRing = keyRing;
this.feeService = feeService;
this.btcFeeReceiverService = btcFeeReceiverService;
this.delayedPayoutTxReceiverService = delayedPayoutTxReceiverService;
}
}

View file

@ -80,6 +80,9 @@ public final class InputsForDepositTxRequest extends TradeMessage implements Dir
@Nullable
private final String takersPaymentMethodId;
// Added in v 1.9.7
private final int burningManSelectionHeight;
public InputsForDepositTxRequest(String tradeId,
NodeAddress senderNodeAddress,
long tradeAmount,
@ -107,7 +110,8 @@ public final class InputsForDepositTxRequest extends TradeMessage implements Dir
byte[] accountAgeWitnessSignatureOfOfferId,
long currentDate,
@Nullable byte[] hashOfTakersPaymentAccountPayload,
@Nullable String takersPaymentMethodId) {
@Nullable String takersPaymentMethodId,
int burningManSelectionHeight) {
super(messageVersion, tradeId, uid);
this.senderNodeAddress = senderNodeAddress;
this.tradeAmount = tradeAmount;
@ -134,6 +138,7 @@ public final class InputsForDepositTxRequest extends TradeMessage implements Dir
this.currentDate = currentDate;
this.hashOfTakersPaymentAccountPayload = hashOfTakersPaymentAccountPayload;
this.takersPaymentMethodId = takersPaymentMethodId;
this.burningManSelectionHeight = burningManSelectionHeight;
}
@ -169,7 +174,8 @@ public final class InputsForDepositTxRequest extends TradeMessage implements Dir
.setRefundAgentNodeAddress(refundAgentNodeAddress.toProtoMessage())
.setUid(uid)
.setAccountAgeWitnessSignatureOfOfferId(ByteString.copyFrom(accountAgeWitnessSignatureOfOfferId))
.setCurrentDate(currentDate);
.setCurrentDate(currentDate)
.setBurningManSelectionHeight(burningManSelectionHeight);
Optional.ofNullable(changeOutputAddress).ifPresent(builder::setChangeOutputAddress);
Optional.ofNullable(arbitratorNodeAddress).ifPresent(e -> builder.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage()));
@ -223,7 +229,8 @@ public final class InputsForDepositTxRequest extends TradeMessage implements Dir
ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignatureOfOfferId()),
proto.getCurrentDate(),
hashOfTakersPaymentAccountPayload,
ProtoUtil.stringOrNullFromProto(proto.getTakersPayoutMethodId()));
ProtoUtil.stringOrNullFromProto(proto.getTakersPayoutMethodId()),
proto.getBurningManSelectionHeight());
}
@Override
@ -253,6 +260,7 @@ public final class InputsForDepositTxRequest extends TradeMessage implements Dir
",\n currentDate=" + currentDate +
",\n hashOfTakersPaymentAccountPayload=" + Utilities.bytesAsHexString(hashOfTakersPaymentAccountPayload) +
",\n takersPaymentMethodId=" + takersPaymentMethodId +
",\n burningManSelectionHeight=" + burningManSelectionHeight +
"\n} " + super.toString();
}
}

View file

@ -23,6 +23,8 @@ import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.dao.DaoFacade;
import bisq.core.dao.burningman.BtcFeeReceiverService;
import bisq.core.dao.burningman.DelayedPayoutTxReceiverService;
import bisq.core.filter.FilterManager;
import bisq.core.network.MessageState;
import bisq.core.offer.Offer;
@ -176,6 +178,11 @@ public class ProcessModel implements ProtocolModel<TradingPeer> {
@Setter
private ObjectProperty<MessageState> paymentStartedMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
// Added in v 1.9.7
@Setter
@Getter
private int burningManSelectionHeight;
public ProcessModel(String offerId, String accountId, PubKeyRing pubKeyRing) {
this(offerId, accountId, pubKeyRing, new TradingPeer());
}
@ -217,7 +224,8 @@ public class ProcessModel implements ProtocolModel<TradingPeer> {
.setFundsNeededForTradeAsLong(fundsNeededForTradeAsLong)
.setPaymentStartedMessageState(paymentStartedMessageStateProperty.get().name())
.setBuyerPayoutAmountFromMediation(buyerPayoutAmountFromMediation)
.setSellerPayoutAmountFromMediation(sellerPayoutAmountFromMediation);
.setSellerPayoutAmountFromMediation(sellerPayoutAmountFromMediation)
.setBurningManSelectionHeight(burningManSelectionHeight);
Optional.ofNullable(takeOfferFeeTxId).ifPresent(builder::setTakeOfferFeeTxId);
Optional.ofNullable(payoutTxSignature).ifPresent(e -> builder.setPayoutTxSignature(ByteString.copyFrom(payoutTxSignature)));
@ -259,6 +267,7 @@ public class ProcessModel implements ProtocolModel<TradingPeer> {
String paymentStartedMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentStartedMessageState());
MessageState paymentStartedMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentStartedMessageStateString);
processModel.setPaymentStartedMessageState(paymentStartedMessageState);
processModel.setBurningManSelectionHeight(proto.getBurningManSelectionHeight());
if (proto.hasPaymentAccount()) {
processModel.setPaymentAccount(PaymentAccount.fromProto(proto.getPaymentAccount(), coreProtoResolver));
@ -377,6 +386,14 @@ public class ProcessModel implements ProtocolModel<TradingPeer> {
return provider.getTradeWalletService();
}
public BtcFeeReceiverService getBtcFeeReceiverService() {
return provider.getBtcFeeReceiverService();
}
public DelayedPayoutTxReceiverService getDelayedPayoutTxReceiverService() {
return provider.getDelayedPayoutTxReceiverService();
}
public User getUser() {
return provider.getUser();
}

View file

@ -17,16 +17,22 @@
package bisq.core.trade.protocol.bisq_v1.tasks.buyer;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.dao.burningman.BurningManService;
import bisq.core.trade.bisq_v1.TradeDataValidation;
import bisq.core.trade.model.bisq_v1.Trade;
import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask;
import bisq.common.taskrunner.TaskRunner;
import bisq.common.util.Tuple2;
import org.bitcoinj.core.Transaction;
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
@ -40,17 +46,37 @@ public class BuyerVerifiesFinalDelayedPayoutTx extends TradeTask {
try {
runInterceptHook();
Transaction delayedPayoutTx = trade.getDelayedPayoutTx();
checkNotNull(delayedPayoutTx, "trade.getDelayedPayoutTx() must not be null");
BtcWalletService btcWalletService = processModel.getBtcWalletService();
Transaction finalDelayedPayoutTx = trade.getDelayedPayoutTx();
checkNotNull(finalDelayedPayoutTx, "trade.getDelayedPayoutTx() must not be null");
// Check again tx
TradeDataValidation.validateDelayedPayoutTx(trade,
delayedPayoutTx,
processModel.getBtcWalletService());
finalDelayedPayoutTx,
btcWalletService);
// Now as we know the deposit tx we can also verify the input
Transaction depositTx = trade.getDepositTx();
checkNotNull(depositTx, "trade.getDepositTx() must not be null");
TradeDataValidation.validatePayoutTxInput(depositTx, delayedPayoutTx);
// Now as we know the deposit tx we can also verify the input
TradeDataValidation.validatePayoutTxInput(depositTx, finalDelayedPayoutTx);
if (BurningManService.isActivated()) {
long inputAmount = depositTx.getOutput(0).getValue().value;
long tradeTxFeeAsLong = trade.getTradeTxFeeAsLong();
int selectionHeight = processModel.getBurningManSelectionHeight();
List<Tuple2<Long, String>> delayedPayoutTxReceivers = processModel.getDelayedPayoutTxReceiverService().getReceivers(
selectionHeight,
inputAmount,
tradeTxFeeAsLong);
log.info("Verify delayedPayoutTx using selectionHeight {} and receivers {}", selectionHeight, delayedPayoutTxReceivers);
long lockTime = trade.getLockTime();
Transaction buyersDelayedPayoutTx = processModel.getTradeWalletService().createDelayedUnsignedPayoutTx(
depositTx,
delayedPayoutTxReceivers,
lockTime);
checkArgument(buyersDelayedPayoutTx.getTxId().equals(finalDelayedPayoutTx.getTxId()),
"TxIds of buyersDelayedPayoutTx and finalDelayedPayoutTx must be the same");
}
complete();
} catch (TradeDataValidation.ValidationException e) {

View file

@ -17,14 +17,22 @@
package bisq.core.trade.protocol.bisq_v1.tasks.buyer;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.dao.burningman.BurningManService;
import bisq.core.trade.bisq_v1.TradeDataValidation;
import bisq.core.trade.model.bisq_v1.Trade;
import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask;
import bisq.common.taskrunner.TaskRunner;
import bisq.common.util.Tuple2;
import org.bitcoinj.core.Transaction;
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
@ -38,19 +46,36 @@ public class BuyerVerifiesPreparedDelayedPayoutTx extends TradeTask {
try {
runInterceptHook();
var preparedDelayedPayoutTx = processModel.getPreparedDelayedPayoutTx();
Transaction sellersPreparedDelayedPayoutTx = checkNotNull(processModel.getPreparedDelayedPayoutTx());
BtcWalletService btcWalletService = processModel.getBtcWalletService();
TradeDataValidation.validateDelayedPayoutTx(trade,
preparedDelayedPayoutTx,
processModel.getBtcWalletService());
sellersPreparedDelayedPayoutTx,
btcWalletService);
Transaction preparedDepositTx = btcWalletService.getTxFromSerializedTx(processModel.getPreparedDepositTx());
if (BurningManService.isActivated()) {
long inputAmount = preparedDepositTx.getOutput(0).getValue().value;
long tradeTxFeeAsLong = trade.getTradeTxFeeAsLong();
List<Tuple2<Long, String>> delayedPayoutTxReceivers = processModel.getDelayedPayoutTxReceiverService().getReceivers(
processModel.getBurningManSelectionHeight(),
inputAmount,
tradeTxFeeAsLong);
long lockTime = trade.getLockTime();
Transaction buyersPreparedDelayedPayoutTx = processModel.getTradeWalletService().createDelayedUnsignedPayoutTx(
preparedDepositTx,
delayedPayoutTxReceivers,
lockTime);
checkArgument(buyersPreparedDelayedPayoutTx.getTxId().equals(sellersPreparedDelayedPayoutTx.getTxId()),
"TxIds of buyersPreparedDelayedPayoutTx and sellersPreparedDelayedPayoutTx must be the same");
}
// If the deposit tx is non-malleable, we already know its final ID, so should check that now
// before sending any further data to the seller, to provide extra protection for the buyer.
if (isDepositTxNonMalleable()) {
var preparedDepositTx = processModel.getBtcWalletService().getTxFromSerializedTx(
processModel.getPreparedDepositTx());
TradeDataValidation.validatePayoutTxInput(preparedDepositTx, checkNotNull(preparedDelayedPayoutTx));
TradeDataValidation.validatePayoutTxInput(preparedDepositTx, sellersPreparedDelayedPayoutTx);
} else {
log.info("Deposit tx is malleable, so we skip preparedDelayedPayoutTx input validation.");
log.info("Deposit tx is malleable, so we skip sellersPreparedDelayedPayoutTx input validation.");
}
complete();

View file

@ -17,6 +17,7 @@
package bisq.core.trade.protocol.bisq_v1.tasks.maker;
import bisq.core.dao.burningman.BurningManService;
import bisq.core.exceptions.TradePriceOutOfToleranceException;
import bisq.core.offer.Offer;
import bisq.core.support.dispute.mediation.mediator.Mediator;
@ -79,6 +80,16 @@ public class MakerProcessesInputsForDepositTxRequest extends TradeTask {
tradingPeer.setAccountId(nonEmptyStringOf(request.getTakerAccountId()));
if (BurningManService.isActivated()) {
int takersBurningManSelectionHeight = request.getBurningManSelectionHeight();
checkArgument(takersBurningManSelectionHeight > 0, "takersBurningManSelectionHeight must not be 0");
int makersBurningManSelectionHeight = processModel.getDelayedPayoutTxReceiverService().getBurningManSelectionHeight();
checkArgument(takersBurningManSelectionHeight == makersBurningManSelectionHeight,
"takersBurningManSelectionHeight does no match makersBurningManSelectionHeight");
processModel.setBurningManSelectionHeight(makersBurningManSelectionHeight);
}
// We set the taker fee only in the processModel yet not in the trade as the tx was only created but not
// published yet. Once it was published we move it to trade. The takerFeeTx should be sent in a later
// message but that cannot be changed due backward compatibility issues. It is a left over from the

View file

@ -18,16 +18,20 @@
package bisq.core.trade.protocol.bisq_v1.tasks.seller;
import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.dao.burningman.BurningManService;
import bisq.core.dao.governance.param.Param;
import bisq.core.trade.bisq_v1.TradeDataValidation;
import bisq.core.trade.model.bisq_v1.Trade;
import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask;
import bisq.common.taskrunner.TaskRunner;
import bisq.common.util.Tuple2;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Transaction;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
@ -44,16 +48,33 @@ public class SellerCreatesDelayedPayoutTx extends TradeTask {
try {
runInterceptHook();
String donationAddressString = processModel.getDaoFacade().getParamValue(Param.RECIPIENT_BTC_ADDRESS);
Coin minerFee = trade.getTradeTxFee();
TradeWalletService tradeWalletService = processModel.getTradeWalletService();
Transaction depositTx = checkNotNull(processModel.getDepositTx());
Transaction preparedDelayedPayoutTx;
if (BurningManService.isActivated()) {
long inputAmount = depositTx.getOutput(0).getValue().value;
long tradeTxFeeAsLong = trade.getTradeTxFeeAsLong();
int selectionHeight = processModel.getBurningManSelectionHeight();
List<Tuple2<Long, String>> delayedPayoutTxReceivers = processModel.getDelayedPayoutTxReceiverService().getReceivers(
selectionHeight,
inputAmount,
tradeTxFeeAsLong);
log.info("Verify delayedPayoutTx using selectionHeight {} and receivers {}", selectionHeight, delayedPayoutTxReceivers);
long lockTime = trade.getLockTime();
Transaction preparedDelayedPayoutTx = tradeWalletService.createDelayedUnsignedPayoutTx(depositTx,
preparedDelayedPayoutTx = tradeWalletService.createDelayedUnsignedPayoutTx(
depositTx,
delayedPayoutTxReceivers,
lockTime);
} else {
String donationAddressString = processModel.getDaoFacade().getParamValue(Param.RECIPIENT_BTC_ADDRESS);
Coin minerFee = trade.getTradeTxFee();
long lockTime = trade.getLockTime();
preparedDelayedPayoutTx = tradeWalletService.createDelayedUnsignedPayoutTx(depositTx,
donationAddressString,
minerFee,
lockTime);
}
TradeDataValidation.validateDelayedPayoutTx(trade,
preparedDelayedPayoutTx,
processModel.getBtcWalletService());

View file

@ -24,7 +24,6 @@ import bisq.core.btc.wallet.WalletService;
import bisq.core.dao.exceptions.DaoDisabledException;
import bisq.core.trade.model.bisq_v1.Trade;
import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask;
import bisq.core.util.FeeReceiverSelector;
import bisq.common.taskrunner.TaskRunner;
@ -66,7 +65,7 @@ public class CreateTakerFeeTx extends TradeTask {
Transaction transaction;
if (trade.isCurrencyForTakerFeeBtc()) {
String feeReceiver = FeeReceiverSelector.getAddress(processModel.getFilterManager());
String feeReceiver = processModel.getBtcFeeReceiverService().getAddress();
transaction = tradeWalletService.createBtcTradingFeeTx(
fundingAddress,
reservedForTradeAddress,

View file

@ -104,6 +104,9 @@ public class TakerSendInputsForDepositTxRequest extends TradeTask {
byte[] signatureOfNonce = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(),
offerId.getBytes(Charsets.UTF_8));
int burningManSelectionHeight = processModel.getDelayedPayoutTxReceiverService().getBurningManSelectionHeight();
processModel.setBurningManSelectionHeight(burningManSelectionHeight);
String takersPaymentMethodId = checkNotNull(processModel.getPaymentAccountPayload(trade)).getPaymentMethodId();
InputsForDepositTxRequest request = new InputsForDepositTxRequest(
offerId,
@ -133,7 +136,8 @@ public class TakerSendInputsForDepositTxRequest extends TradeTask {
signatureOfNonce,
new Date().getTime(),
hashOfTakersPaymentAccountPayload,
takersPaymentMethodId);
takersPaymentMethodId,
burningManSelectionHeight);
log.info("Send {} with offerId {} and uid {} to peer {}",
request.getClass().getSimpleName(), request.getTradeId(),
request.getUid(), trade.getTradingPeerNodeAddress());

View file

@ -42,11 +42,26 @@ public class AveragePriceUtil {
public static Tuple2<Price, Price> getAveragePriceTuple(Preferences preferences,
TradeStatisticsManager tradeStatisticsManager,
int days) {
return getAveragePriceTuple(preferences, tradeStatisticsManager, days, new Date());
}
public static Tuple2<Price, Price> getAveragePriceTuple(Preferences preferences,
TradeStatisticsManager tradeStatisticsManager,
int days,
Date date) {
Date pastXDays = getPastDate(days, date);
return getAveragePriceTuple(preferences, tradeStatisticsManager, pastXDays, date);
}
public static Tuple2<Price, Price> getAveragePriceTuple(Preferences preferences,
TradeStatisticsManager tradeStatisticsManager,
Date pastXDays,
Date date) {
double percentToTrim = Math.max(0, Math.min(49, preferences.getBsqAverageTrimThreshold() * 100));
Date pastXDays = getPastDate(days);
List<TradeStatistics3> bsqAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
.filter(e -> e.getCurrency().equals("BSQ"))
.filter(e -> e.getDate().after(pastXDays))
.filter(e -> e.getDate().before(date))
.collect(Collectors.toList());
List<TradeStatistics3> bsqTradePastXDays = percentToTrim > 0 ?
removeOutliers(bsqAllTradePastXDays, percentToTrim) :
@ -55,6 +70,7 @@ public class AveragePriceUtil {
List<TradeStatistics3> usdAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
.filter(e -> e.getCurrency().equals("USD"))
.filter(e -> e.getDate().after(pastXDays))
.filter(e -> e.getDate().before(date))
.collect(Collectors.toList());
List<TradeStatistics3> usdTradePastXDays = percentToTrim > 0 ?
removeOutliers(usdAllTradePastXDays, percentToTrim) :
@ -130,9 +146,9 @@ public class AveragePriceUtil {
return averagePrice;
}
private static Date getPastDate(int days) {
private static Date getPastDate(int days, Date date) {
Calendar cal = new GregorianCalendar();
cal.setTime(new Date());
cal.setTime(date);
cal.add(Calendar.DAY_OF_MONTH, -1 * days);
return cal.getTime();
}

View file

@ -1,86 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.util;
import bisq.core.filter.FilterManager;
import bisq.common.config.Config;
import org.bitcoinj.core.Coin;
import com.google.common.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class FeeReceiverSelector {
public static final String BTC_FEE_RECEIVER_ADDRESS = "38bZBj5peYS3Husdz7AH3gEUiUbYRD951t";
public static String getMostRecentAddress() {
return Config.baseCurrencyNetwork().isMainnet() ? BTC_FEE_RECEIVER_ADDRESS :
Config.baseCurrencyNetwork().isTestnet() ? "2N4mVTpUZAnhm9phnxB7VrHB4aBhnWrcUrV" :
"2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w";
}
public static String getAddress(FilterManager filterManager) {
return getAddress(filterManager, new Random());
}
@VisibleForTesting
static String getAddress(FilterManager filterManager, Random rnd) {
List<String> feeReceivers = Optional.ofNullable(filterManager.getFilter())
.flatMap(f -> Optional.ofNullable(f.getBtcFeeReceiverAddresses()))
.orElse(List.of());
List<Long> amountList = new ArrayList<>();
List<String> receiverAddressList = new ArrayList<>();
feeReceivers.forEach(e -> {
try {
String[] tokens = e.split("#");
amountList.add(Coin.parseCoin(tokens[1]).longValue()); // total amount the victim should receive
receiverAddressList.add(tokens[0]); // victim's receiver address
} catch (RuntimeException ignore) {
// If input format is not as expected we ignore entry
}
});
if (!amountList.isEmpty()) {
return receiverAddressList.get(weightedSelection(amountList, rnd));
}
// If no fee address receiver is defined via filter we use the hard coded recent address
return getMostRecentAddress();
}
@VisibleForTesting
static int weightedSelection(List<Long> weights, Random rnd) {
long sum = weights.stream().mapToLong(n -> n).sum();
long target = rnd.longs(0, sum).findFirst().orElseThrow();
int i;
for (i = 0; i < weights.size() && target >= 0; i++) {
target -= weights.get(i);
}
return i - 1;
}
}

View file

@ -221,6 +221,10 @@ public class BsqFormatter implements CoinFormatter {
}
}
public String formatCoin(long value) {
return formatCoin(Coin.valueOf(value));
}
public String formatCoin(Coin coin) {
return formatCoin(coin, false);
}

View file

@ -5,6 +5,8 @@ import org.bitcoinj.core.Coin;
public interface CoinFormatter {
String formatCoin(Coin coin);
String formatCoin(long value);
String formatCoin(Coin coin, boolean appendCode);
String formatCoin(Coin coin, int decimalPlaces);

View file

@ -53,6 +53,11 @@ public class ImmutableCoinFormatter implements CoinFormatter {
return formatCoin(coin, -1);
}
@Override
public String formatCoin(long value) {
return formatCoin(Coin.valueOf(value));
}
@Override
public String formatCoin(Coin coin, boolean appendCode) {
return appendCode ? formatCoinWithCode(coin) : formatCoin(coin);

View file

@ -43,6 +43,9 @@ public final class BtcAddressValidator extends InputValidator {
}
private ValidationResult validateBtcAddress(String input) {
if (allowEmpty && (input == null || input.trim().isEmpty())) {
return new ValidationResult(true);
}
try {
Address.fromString(Config.baseCurrencyNetworkParameters(), input);
return new ValidationResult(true);

View file

@ -24,7 +24,11 @@ import java.math.BigInteger;
import java.util.Objects;
import java.util.function.Function;
import lombok.Setter;
public class InputValidator {
@Setter
public boolean allowEmpty;
public ValidationResult validate(String input) {
return validateIfNotEmpty(input);
@ -32,10 +36,13 @@ public class InputValidator {
protected ValidationResult validateIfNotEmpty(String input) {
//trim added to avoid empty input
if (input == null || input.trim().length() == 0)
return new ValidationResult(false, Res.get("validation.empty"));
else
if (allowEmpty) {
return new ValidationResult(true);
} else if (input == null || input.trim().length() == 0) {
return new ValidationResult(false, Res.get("validation.empty"));
} else {
return new ValidationResult(true);
}
}
public static class ValidationResult {

View file

@ -975,14 +975,15 @@ portfolio.pending.mediationResult.popup.info=The mediator has suggested the foll
Your trading peer receives: {1}\n\n\
You can accept or reject this suggested payout.\n\n\
By accepting, you sign the proposed payout transaction. \
Mediation is expected to be the optimal resolution for both traders. \
If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\n\
If one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a \
second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\n\
The arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. \
Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for \
exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion \
(or if the other peer is unresponsive).\n\n\
More details about the new arbitration model: [HYPERLINK:https://bisq.wiki/Dispute_resolution#Level_3:_Arbitration]
If one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to reject mediation suggestion, \
which will open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\n\
If the trade goes to arbitration the arbitrator will pay out the trade amount plus one peer's security deposit. \
This means the total arbitration payout will be less than the mediation payout. \
Requesting arbitration is meant for exceptional circumstances. such as; \
one peer not responding, or disputing the mediator made a fair payout suggestion. \n\n\
More details about the arbitration model: [HYPERLINK:https://bisq.wiki/Dispute_resolution#Level_3:_Arbitration]
portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout \
but it seems that your trading peer has not accepted it.\n\n\
Once the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will \
@ -1967,7 +1968,7 @@ dao.tab.factsAndFigures=Facts & Figures
dao.tab.bsqWallet=BSQ wallet
dao.tab.proposals=Governance
dao.tab.bonding=Bonding
dao.tab.proofOfBurn=Asset listing fee/Proof of burn
dao.tab.proofOfBurn=Proof of burn
dao.tab.monitor=Network monitor
dao.tab.news=News
@ -2097,7 +2098,8 @@ dao.param.ASSET_LISTING_FEE_PER_DAY=Asset listing fee per day
dao.param.ASSET_MIN_VOLUME=Min. trade volume for assets
# suppress inspection "UnusedProperty"
dao.param.LOCK_TIME_TRADE_PAYOUT=Lock time for alternative trade payout tx
## Was never used and got re-purposed for expected BTC fee revenue per cycle
dao.param.LOCK_TIME_TRADE_PAYOUT=BTC fee revenue / cycle as BSQ Satoshi value
# suppress inspection "UnusedProperty"
dao.param.ARBITRATOR_FEE=Arbitrator fee in BTC
@ -2261,6 +2263,7 @@ dao.bond.bondedRoleType.BTC_DONATION_ADDRESS_OWNER=BTC donation address owner
dao.burnBsq.assetFee=Asset listing
dao.burnBsq.menuItem.assetFee=Asset listing fee
dao.burnBsq.menuItem.proofOfBurn=Proof of burn
dao.burnBsq.menuItem.burningMan=Burningmen
dao.burnBsq.header=Fee for asset listing
dao.burnBsq.selectAsset=Select Asset
dao.burnBsq.fee=Fee
@ -2275,6 +2278,59 @@ dao.burnBsq.assets.trialFee=Fee for trial period
dao.burnBsq.assets.totalFee=Total fees paid
dao.burnBsq.assets.days={0} days
dao.burnBsq.assets.toFewDays=The asset fee is too low. The min. amount of days for the trial period is {0}.
dao.burningman.target.header=Burn target
dao.burningman.burn.header=Burn BSQ
dao.burningman.amount.prompt=Amount to burn
dao.burningman.amount.prompt.max=Amount to burn: {0} - {1} BSQ
dao.burningman.candidates.table.header=Burningmen candidates
dao.burningman.burnOutput.table.header=Burn outputs
dao.burningman.balanceEntry.table.header=Revenue
dao.burningman.balanceEntry.table.showMonthlyToggle=By month
dao.burningman.balanceEntry.table.radio.all=All
dao.burningman.balanceEntry.table.radio.fee=BTC fees only
dao.burningman.balanceEntry.table.radio.dpt=DPT only
dao.burningman.balanceEntry.table.radio.burn=Burned BSQ
dao.burningman.compensations.table.header=Compensations
dao.burningman.reimbursement.table.header=Reimbursement requests
dao.burningman.expectedRevenue=Past 3-month average distribution
dao.burningman.burnTarget.label=Amount of BSQ to burn
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.selectedContributor=Selected contributor
dao.burningman.selectedContributorName=Contributor name
dao.burningman.selectedContributorAddress=Receiver address
dao.burningman.shared.table.height=Block height
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.expectedRevenue=Expected to receive
dao.burningman.table.burnAmount=Burned amount
dao.burningman.table.decayedBurnAmount=Decayed burned amount
dao.burningman.table.burnAmountShare.label=Receiver share
dao.burningman.table.burnAmountShare.capped={0} (capped from {1})
dao.burningman.table.numBurnOutputs=Num. Burns
dao.burningman.table.issuanceAmount=Issued amount
dao.burningman.table.decayedIssuanceAmount=Decayed issued amount
dao.burningman.table.issuanceShare=Issuance share
dao.burningman.table.numIssuances=Num. issuances
dao.burningman.table.reimbursedAmount=Reimbursed amount
dao.burningman.table.balanceEntry.date=Date
dao.burningman.table.balanceEntry.receivedBtc=Received BTC
dao.burningman.table.balanceEntry.receivedBtcAsBsq=Received BTC as BSQ
dao.burningman.table.balanceEntry.burnedBsq=Burned BSQ
dao.burningman.table.balanceEntry.revenue=Revenue
dao.burningman.table.balanceEntry.price=BSQ/BTC price
dao.burningman.table.balanceEntry.type=Type
# From BalanceEntry.Type enum names
dao.burningman.balanceEntry.type.UNDEFINED=Undefined
dao.burningman.balanceEntry.type.BTC_TRADE_FEE_TX=Trade fee
dao.burningman.balanceEntry.type.DPT_TX=DPT
dao.burningman.balanceEntry.type.BURN_TX=Burned BSQ
# suppress inspection "UnusedProperty"
dao.assetState.UNDEFINED=Undefined
@ -2400,6 +2456,7 @@ dao.proposal.display.name=Exact GitHub username
dao.proposal.display.link=Link to detailed info
dao.proposal.display.link.prompt=Link to proposal
dao.proposal.display.requestedBsq=Requested amount in BSQ
dao.proposal.display.burningManReceiverAddress=Burningman receiver address (optional)
dao.proposal.display.txId=Proposal transaction ID
dao.proposal.display.proposalFee=Proposal fee
dao.proposal.display.myVote=My vote
@ -2459,6 +2516,10 @@ dao.wallet.send.receiverBtcAddress=Receiver's BTC address
dao.wallet.send.setDestinationAddress=Fill in your destination address
dao.wallet.send.send=Send BSQ funds
dao.wallet.send.inputControl=Select inputs
dao.wallet.send.addOpReturn=Add data
dao.wallet.send.preImage=Pre-image
dao.wallet.send.opReturnAsHex=Op-Return data Hex encoded
dao.wallet.send.opReturnAsHash=Hash of Op-Return data
dao.wallet.send.sendBtc=Send BTC funds
dao.wallet.send.sendFunds.headline=Confirm withdrawal request
dao.wallet.send.sendFunds.details=Sending: {0}\nTo receiving address: {1}.\nRequired mining fee is: {2} ({3} satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nThe recipient will receive: {5}\n\nAre you sure you want to withdraw that amount?
@ -2758,7 +2819,7 @@ disputeSummaryWindow.requestingTxs=Requesting blockchain transactions from block
disputeSummaryWindow.requestTransactionsError=Requesting the 4 trade transactions failed. Error message: {0}.\n\n\
Please verify the transactions manually before closing the dispute.
disputeSummaryWindow.delayedPayoutTxVerificationFailed=Verification of the delayed payout transaction failed. Error message: {0}.\n\n\
Please do not make the payout but get in touch with developers to clearify the case.
Please do not make the payout but get in touch with developers to clarify the case.
# dynamic values are not recognized by IntelliJ
# suppress inspection "UnusedProperty"

View file

@ -0,0 +1,80 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman;
import com.google.common.primitives.Longs;
import java.util.List;
import java.util.Random;
import org.mockito.junit.MockitoJUnitRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
@RunWith(MockitoJUnitRunner.class)
public class BtcFeeReceiverServiceTest {
@Test
public void testGetRandomIndex() {
Random rnd = new Random(456);
assertEquals(4, BtcFeeReceiverService.getRandomIndex(Longs.asList(0, 0, 0, 3, 3), rnd));
assertEquals(3, BtcFeeReceiverService.getRandomIndex(Longs.asList(0, 0, 0, 6, 0, 0, 0, 0, 0), rnd));
assertEquals(-1, BtcFeeReceiverService.getRandomIndex(Longs.asList(), rnd));
assertEquals(-1, BtcFeeReceiverService.getRandomIndex(Longs.asList(0), rnd));
assertEquals(-1, BtcFeeReceiverService.getRandomIndex(Longs.asList(0, 0), rnd));
int[] selections = new int[3];
for (int i = 0; i < 6000; i++) {
selections[BtcFeeReceiverService.getRandomIndex(Longs.asList(1, 2, 3), rnd)]++;
}
// selections with new Random(456) are: [986, 1981, 3033]
assertEquals(1000.0, selections[0], 100);
assertEquals(2000.0, selections[1], 100);
assertEquals(3000.0, selections[2], 100);
}
@Test
public void testFindIndex() {
List<Long> weights = Longs.asList(1, 2, 3);
assertEquals(0, BtcFeeReceiverService.findIndex(weights, 1));
assertEquals(1, BtcFeeReceiverService.findIndex(weights, 2));
assertEquals(1, BtcFeeReceiverService.findIndex(weights, 3));
assertEquals(2, BtcFeeReceiverService.findIndex(weights, 4));
assertEquals(2, BtcFeeReceiverService.findIndex(weights, 5));
assertEquals(2, BtcFeeReceiverService.findIndex(weights, 6));
// invalid values return index 0
assertEquals(0, BtcFeeReceiverService.findIndex(weights, 0));
assertEquals(0, BtcFeeReceiverService.findIndex(weights, 7));
assertEquals(0, BtcFeeReceiverService.findIndex(Longs.asList(0, 1, 2, 3), 0));
assertEquals(0, BtcFeeReceiverService.findIndex(Longs.asList(1, 2, 3), 0));
assertEquals(0, BtcFeeReceiverService.findIndex(Longs.asList(1, 2, 3), 1));
assertEquals(1, BtcFeeReceiverService.findIndex(Longs.asList(0, 1, 2, 3), 1));
assertEquals(2, BtcFeeReceiverService.findIndex(Longs.asList(0, 1, 2, 3), 2));
assertEquals(1, BtcFeeReceiverService.findIndex(Longs.asList(0, 1, 0, 2, 3), 1));
assertEquals(3, BtcFeeReceiverService.findIndex(Longs.asList(0, 1, 0, 2, 3), 2));
assertEquals(3, BtcFeeReceiverService.findIndex(Longs.asList(0, 0, 0, 1, 2, 3), 1));
assertEquals(4, BtcFeeReceiverService.findIndex(Longs.asList(0, 0, 0, 1, 2, 3), 2));
assertEquals(6, BtcFeeReceiverService.findIndex(Longs.asList(0, 0, 0, 1, 0, 0, 2, 3), 2));
}
}

View file

@ -0,0 +1,135 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman;
import org.mockito.junit.MockitoJUnitRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
@RunWith(MockitoJUnitRunner.class)
public class BurningManPresentationServiceTest {
@Test
public void testGetRandomIndex() {
long total = 100;
long myAmount = 40;
double myTargetShare = 0.75;
// Initial state:
// Mine: 40
// Others: 60
// Total: 100
// Current myShare: 0.4
// Target state:
// Mine: 40 + 140 = 180
// Others: 60
// Total: 240
// Target myShare: 0.75
assertEquals(140, BurningManPresentationService.getMissingAmountToReachTargetShare(total, myAmount, myTargetShare));
total = 60;
myAmount = 0;
myTargetShare = 0.4;
// Initial state:
// Mine: 0
// Others: 60
// Total: 60
// Current myShare: 0
// Target state:
// Mine: 0 + 40 = 40
// Others: 60
// Total: 100
// Target myShare: 0.4
assertEquals(40, BurningManPresentationService.getMissingAmountToReachTargetShare(total, myAmount, myTargetShare));
total = 40;
myAmount = 40;
myTargetShare = 1;
// Initial state:
// Mine: 40
// Others: 0
// Total: 40
// Current myShare: 1
// Target state:
// Mine: 40 -40 = 0
// Others: 0
// Total: 0
// Target myShare: 1
assertEquals(-40, BurningManPresentationService.getMissingAmountToReachTargetShare(total, myAmount, myTargetShare));
total = 100;
myAmount = 99;
myTargetShare = 0;
// Initial state:
// Mine: 99
// Others: 1
// Total: 100
// Current myShare: 0.99
// Target state:
// Mine: 99 - 99 = 0
// Others: 1
// Total: 100
// Target myShare: 0
assertEquals(-99, BurningManPresentationService.getMissingAmountToReachTargetShare(total, myAmount, myTargetShare));
total = 100;
myAmount = 1;
myTargetShare = 0.5;
// Initial state:
// Mine: 1
// Others: 99
// Total: 100
// Current myShare: 0.01
// Target state:
// Mine: 1 + 98 = 99
// Others: 99
// Total: 198
// Target myShare: 0.5
assertEquals(98, BurningManPresentationService.getMissingAmountToReachTargetShare(total, myAmount, myTargetShare));
total = 110;
myAmount = 0;
myTargetShare = 0.6;
// Initial state:
// Mine: 0
// Others: 110
// Total: 110
// Current myShare: 0
// Target state:
// Mine: 165
// Others: 110
// Total: 275
// Target myShare: 0.6
assertEquals(165, BurningManPresentationService.getMissingAmountToReachTargetShare(total, myAmount, myTargetShare));
}
}

View file

@ -0,0 +1,52 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman;
import org.mockito.junit.MockitoJUnitRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
@RunWith(MockitoJUnitRunner.class)
public class BurningManServiceTest {
@Test
public void testGetDecayedAmount() {
long amount = 100;
int currentBlockHeight = 1400;
int fromBlockHeight = 1000;
assertEquals(0, BurningManService.getDecayedAmount(amount, 1000, currentBlockHeight, fromBlockHeight));
assertEquals(25, BurningManService.getDecayedAmount(amount, 1100, currentBlockHeight, fromBlockHeight));
assertEquals(50, BurningManService.getDecayedAmount(amount, 1200, currentBlockHeight, fromBlockHeight));
assertEquals(75, BurningManService.getDecayedAmount(amount, 1300, currentBlockHeight, fromBlockHeight));
// cycles with 100 blocks, issuance at block 20, look-back period 3 cycles
assertEquals(40, BurningManService.getDecayedAmount(amount, 120, 300, 0));
assertEquals(33, BurningManService.getDecayedAmount(amount, 120, 320, 20));
assertEquals(27, BurningManService.getDecayedAmount(amount, 120, 340, 40));
assertEquals(20, BurningManService.getDecayedAmount(amount, 120, 360, 60));
assertEquals(13, BurningManService.getDecayedAmount(amount, 120, 380, 80));
assertEquals(7, BurningManService.getDecayedAmount(amount, 120, 399, 99));
assertEquals(7, BurningManService.getDecayedAmount(amount, 120, 400, 100));
assertEquals(3, BurningManService.getDecayedAmount(amount, 120, 410, 110));
assertEquals(40, BurningManService.getDecayedAmount(amount, 220, 400, 100));
}
}

View file

@ -0,0 +1,60 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao.burningman;
import org.mockito.junit.MockitoJUnitRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
@RunWith(MockitoJUnitRunner.class)
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(120, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 133, 10));
assertEquals(120, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 134, 10));
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(140, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 149, 10));
assertEquals(140, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 150, 10));
assertEquals(140, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 151, 10));
assertEquals(150, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 159, 10));
assertEquals(990, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 1000, 10));
}
}

View file

@ -67,6 +67,8 @@ public class OpenOfferManagerTest {
null,
null,
null,
null,
null,
persistenceManager
);
@ -114,6 +116,8 @@ public class OpenOfferManagerTest {
null,
null,
null,
null,
null,
persistenceManager
);
@ -153,6 +157,8 @@ public class OpenOfferManagerTest {
null,
null,
null,
null,
null,
persistenceManager
);

View file

@ -22,7 +22,6 @@ import bisq.core.dao.state.DaoStateService;
import bisq.core.filter.Filter;
import bisq.core.filter.FilterManager;
import bisq.core.trade.DelayedPayoutAddressProvider;
import bisq.core.util.FeeReceiverSelector;
import bisq.core.util.ParsingUtils;
import bisq.core.util.coin.BsqFormatter;
@ -65,7 +64,7 @@ public class TxValidatorTest {
btcFeeReceivers.add("13sxMq8mTw7CTSqgGiMPfwo6ZDsVYrHLmR");
btcFeeReceivers.add("19qA2BVPoyXDfHKVMovKG7SoxGY7xrBV8c");
btcFeeReceivers.add("19BNi5EpZhgBBWAt5ka7xWpJpX2ZWJEYyq");
btcFeeReceivers.add(FeeReceiverSelector.BTC_FEE_RECEIVER_ADDRESS);
btcFeeReceivers.add("38bZBj5peYS3Husdz7AH3gEUiUbYRD951t");
btcFeeReceivers.add(DelayedPayoutAddressProvider.BM2019_ADDRESS);
btcFeeReceivers.add("1BVxNn3T12veSK6DgqwU4Hdn7QHcDDRag7");
btcFeeReceivers.add("3A8Zc1XioE2HRzYfbb5P8iemCS72M6vRJV");

View file

@ -1,142 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.util;
import bisq.core.filter.Filter;
import bisq.core.filter.FilterManager;
import com.google.common.collect.Lists;
import com.google.common.primitives.Longs;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class FeeReceiverSelectorTest {
@Mock
private FilterManager filterManager;
@Test
public void testGetAddress() {
Random rnd = new Random(123);
when(filterManager.getFilter()).thenReturn(filterWithReceivers(
List.of("", "foo#0.001", "ill-formed", "bar#0.002", "baz#0.001", "partial#bad")));
Map<String, Integer> selectionCounts = new HashMap<>();
for (int i = 0; i < 400; i++) {
String address = FeeReceiverSelector.getAddress(filterManager, rnd);
selectionCounts.compute(address, (k, n) -> n != null ? n + 1 : 1);
}
assertEquals(3, selectionCounts.size());
// Check within 2 std. of the expected values (95% confidence each):
assertEquals(100.0, selectionCounts.get("foo"), 18);
assertEquals(200.0, selectionCounts.get("bar"), 20);
assertEquals(100.0, selectionCounts.get("baz"), 18);
}
@Test
public void testGetAddress_noValidReceivers_nullFilter() {
when(filterManager.getFilter()).thenReturn(null);
assertEquals(FeeReceiverSelector.getMostRecentAddress(), FeeReceiverSelector.getAddress(filterManager));
}
@Test
public void testGetAddress_noValidReceivers_filterWithNullList() {
when(filterManager.getFilter()).thenReturn(filterWithReceivers(null));
assertEquals(FeeReceiverSelector.getMostRecentAddress(), FeeReceiverSelector.getAddress(filterManager));
}
@Test
public void testGetAddress_noValidReceivers_filterWithEmptyList() {
when(filterManager.getFilter()).thenReturn(filterWithReceivers(List.of()));
assertEquals(FeeReceiverSelector.getMostRecentAddress(), FeeReceiverSelector.getAddress(filterManager));
}
@Test
public void testGetAddress_noValidReceivers_filterWithIllFormedList() {
when(filterManager.getFilter()).thenReturn(filterWithReceivers(List.of("ill-formed")));
assertEquals(FeeReceiverSelector.getMostRecentAddress(), FeeReceiverSelector.getAddress(filterManager));
}
@Test
public void testWeightedSelection() {
Random rnd = new Random(456);
int[] selections = new int[3];
for (int i = 0; i < 6000; i++) {
selections[FeeReceiverSelector.weightedSelection(Longs.asList(1, 2, 3), rnd)]++;
}
// Check within 2 std. of the expected values (95% confidence each):
assertEquals(1000.0, selections[0], 58);
assertEquals(2000.0, selections[1], 74);
assertEquals(3000.0, selections[2], 78);
}
private static Filter filterWithReceivers(List<String> btcFeeReceiverAddresses) {
return new Filter(Lists.newArrayList(),
Lists.newArrayList(),
Lists.newArrayList(),
Lists.newArrayList(),
Lists.newArrayList(),
Lists.newArrayList(),
Lists.newArrayList(),
Lists.newArrayList(),
false,
Lists.newArrayList(),
false,
null,
null,
Lists.newArrayList(),
Lists.newArrayList(),
Lists.newArrayList(),
btcFeeReceiverAddresses,
null,
0,
null,
null,
null,
null,
false,
Lists.newArrayList(),
new HashSet<>(),
false,
false,
false,
0,
Lists.newArrayList(),
0,
0,
0,
0);
}
}

View file

@ -93,7 +93,8 @@ public class BondsView extends ActivatableView<GridPane, Void> {
@Override
public void initialize() {
tableView = FormBuilder.addTableViewWithHeader(root, ++gridRow, Res.get("dao.bond.allBonds.header"), "last");
tableView = FormBuilder.<BondListItem>addTableViewWithHeader(root, ++gridRow,
Res.get("dao.bond.allBonds.header"), "last").first;
tableView.setItems(sortedList);
GridPane.setVgrow(tableView, Priority.ALWAYS);
addColumns();

View file

@ -141,7 +141,8 @@ public class MyReputationView extends ActivatableView<GridPane, Void> implements
lockupButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.bond.reputation.lockupButton"));
tableView = FormBuilder.addTableViewWithHeader(root, ++gridRow, Res.get("dao.bond.reputation.table.header"), 20, "last");
tableView = FormBuilder.<MyReputationListItem>addTableViewWithHeader(root, ++gridRow,
Res.get("dao.bond.reputation.table.header"), 20, "last").first;
createColumns();
tableView.setItems(sortedList);
GridPane.setVgrow(tableView, Priority.ALWAYS);

View file

@ -23,9 +23,9 @@ import bisq.desktop.components.AutoTooltipButton;
import bisq.desktop.components.AutoTooltipTableColumn;
import bisq.desktop.components.ExternalHyperlink;
import bisq.desktop.components.HyperlinkWithIcon;
import bisq.desktop.main.dao.bonding.BondingViewUtils;
import bisq.desktop.main.dao.MessageSignatureWindow;
import bisq.desktop.main.dao.MessageVerificationWindow;
import bisq.desktop.main.dao.bonding.BondingViewUtils;
import bisq.desktop.util.FormBuilder;
import bisq.desktop.util.GUIUtil;
@ -92,7 +92,8 @@ public class RolesView extends ActivatableView<GridPane, Void> {
@Override
public void initialize() {
int gridRow = 0;
tableView = FormBuilder.addTableViewWithHeader(root, gridRow, Res.get("dao.bond.bondedRoles"), "last");
tableView = FormBuilder.<RolesListItem>addTableViewWithHeader(root, gridRow,
Res.get("dao.bond.bondedRoles"), "last").first;
createColumns();
tableView.setItems(sortedList);
GridPane.setVgrow(tableView, Priority.ALWAYS);
@ -294,6 +295,7 @@ public class RolesView extends ActivatableView<GridPane, Void> {
public TableCell<RolesListItem, RolesListItem> call(TableColumn<RolesListItem, RolesListItem> column) {
return new TableCell<>() {
HBox hbox;
@Override
public void updateItem(final RolesListItem item, boolean empty) {
super.updateItem(item, empty);
@ -315,13 +317,15 @@ public class RolesView extends ActivatableView<GridPane, Void> {
if (item.isLockupButtonVisible()) {
AutoTooltipButton buttonLockup = new AutoTooltipButton(Res.get("dao.bond.table.button.lockup"));
buttonLockup.setMinWidth(70);
buttonLockup.setOnAction(e -> bondingViewUtils.lockupBondForBondedRole(item.getRole(), txId -> {}));
buttonLockup.setOnAction(e -> bondingViewUtils.lockupBondForBondedRole(item.getRole(), txId -> {
}));
hbox.getChildren().add(buttonLockup);
}
if (item.isRevokeButtonVisible()) {
AutoTooltipButton buttonRevoke = new AutoTooltipButton(Res.get("dao.bond.table.button.revoke"));
buttonRevoke.setMinWidth(70);
buttonRevoke.setOnAction(e -> bondingViewUtils.unLock(item.getLockupTxId(), txId -> {}));
buttonRevoke.setOnAction(e -> bondingViewUtils.unLock(item.getLockupTxId(), txId -> {
}));
hbox.getChildren().add(buttonRevoke);
}
hbox.setMinWidth(hbox.getChildren().size() * 70);

View file

@ -28,6 +28,7 @@ import bisq.desktop.components.MenuItem;
import bisq.desktop.main.MainView;
import bisq.desktop.main.dao.DaoView;
import bisq.desktop.main.dao.burnbsq.assetfee.AssetFeeView;
import bisq.desktop.main.dao.burnbsq.burningman.BurningManView;
import bisq.desktop.main.dao.burnbsq.proofofburn.ProofOfBurnView;
import bisq.core.locale.Res;
@ -49,7 +50,7 @@ public class BurnBsqView extends ActivatableView<AnchorPane, Void> {
private final ViewLoader viewLoader;
private final Navigation navigation;
private MenuItem assetFee, proofOfBurn;
private MenuItem proofOfBurn, burningMan, assetFee;
private Navigation.Listener listener;
@FXML
@ -77,26 +78,28 @@ public class BurnBsqView extends ActivatableView<AnchorPane, Void> {
};
toggleGroup = new ToggleGroup();
final List<Class<? extends View>> baseNavPath = Arrays.asList(MainView.class, DaoView.class, BurnBsqView.class);
assetFee = new MenuItem(navigation, toggleGroup, Res.get("dao.burnBsq.menuItem.assetFee"),
AssetFeeView.class, baseNavPath);
List<Class<? extends View>> baseNavPath = Arrays.asList(MainView.class, DaoView.class, BurnBsqView.class);
proofOfBurn = new MenuItem(navigation, toggleGroup, Res.get("dao.burnBsq.menuItem.proofOfBurn"),
ProofOfBurnView.class, baseNavPath);
leftVBox.getChildren().addAll(assetFee, proofOfBurn);
burningMan = new MenuItem(navigation, toggleGroup, Res.get("dao.burnBsq.menuItem.burningMan"),
BurningManView.class, baseNavPath);
assetFee = new MenuItem(navigation, toggleGroup, Res.get("dao.burnBsq.menuItem.assetFee"),
AssetFeeView.class, baseNavPath);
leftVBox.getChildren().addAll(burningMan, proofOfBurn, assetFee);
}
@Override
protected void activate() {
assetFee.activate();
proofOfBurn.activate();
burningMan.activate();
assetFee.activate();
navigation.addListener(listener);
ViewPath viewPath = navigation.getCurrentPath();
if (viewPath.size() == 3 && viewPath.indexOf(BurnBsqView.class) == 2 ||
viewPath.size() == 2 && viewPath.indexOf(DaoView.class) == 1) {
if (selectedViewClass == null)
selectedViewClass = AssetFeeView.class;
selectedViewClass = BurningManView.class;
loadView(selectedViewClass);
@ -111,15 +114,17 @@ public class BurnBsqView extends ActivatableView<AnchorPane, Void> {
protected void deactivate() {
navigation.removeListener(listener);
assetFee.deactivate();
proofOfBurn.deactivate();
burningMan.deactivate();
assetFee.deactivate();
}
private void loadView(Class<? extends View> viewClass) {
View view = viewLoader.load(viewClass);
content.getChildren().setAll(view.getRoot());
if (view instanceof AssetFeeView) toggleGroup.selectToggle(assetFee);
else if (view instanceof ProofOfBurnView) toggleGroup.selectToggle(proofOfBurn);
if (view instanceof ProofOfBurnView) toggleGroup.selectToggle(proofOfBurn);
else if (view instanceof BurningManView) toggleGroup.selectToggle(burningMan);
else if (view instanceof AssetFeeView) toggleGroup.selectToggle(assetFee);
}
}

Some files were not shown because too many files have changed in this diff Show more