Skip to content

Commit

Permalink
Merge pull request #7203 from stejbac/check-receiver-address-validity
Browse files Browse the repository at this point in the history
Check burning man receiver address validity
  • Loading branch information
alejandrogarcia83 authored Jul 19, 2024
2 parents bc31f20 + b10ccd2 commit b8e3296
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@

import com.google.common.annotations.VisibleForTesting;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -69,7 +68,7 @@ private void applyBlock(Block block) {
///////////////////////////////////////////////////////////////////////////////////////////

public String getAddress() {
List<BurningManCandidate> activeBurningManCandidates = new ArrayList<>(burningManService.getActiveBurningManCandidates(currentChainHeight));
List<BurningManCandidate> activeBurningManCandidates = burningManService.getActiveBurningManCandidates(currentChainHeight);
if (activeBurningManCandidates.isEmpty()) {
// If there are no compensation requests (e.g. at dev testing) we fall back to the default address
return burningManService.getLegacyBurningManAddress(currentChainHeight);
Expand Down Expand Up @@ -97,9 +96,7 @@ public String getAddress() {
// the burningManCandidates as we added for the legacy BM an entry at the end.
return burningManService.getLegacyBurningManAddress(currentChainHeight);
}
// For the fee selection we do not need to wait for activation date of the bugfix for
// the receiver address (https://github.com/bisq-network/bisq/issues/6699) as it has no impact on the trade protocol.
return activeBurningManCandidates.get(winnerIndex).getReceiverAddress(true)
return activeBurningManCandidates.get(winnerIndex).getReceiverAddress()
.orElse(burningManService.getLegacyBurningManAddress(currentChainHeight));
}

Expand Down
164 changes: 81 additions & 83 deletions core/src/main/java/bisq/core/dao/burningman/BurningManService.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@
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.util.Tuple2;

import javax.inject.Inject;
import javax.inject.Singleton;

Expand All @@ -52,8 +53,8 @@
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import lombok.extern.slf4j.Slf4j;

Expand Down Expand Up @@ -115,53 +116,48 @@ public BurningManService(DaoStateService daoStateService,
///////////////////////////////////////////////////////////////////////////////////////////

Map<String, BurningManCandidate> getBurningManCandidatesByName(int chainHeight) {
return getBurningManCandidatesByName(chainHeight, !DelayedPayoutTxReceiverService.isProposal412Activated());
return getBurningManCandidatesByName(chainHeight, false);
}

Map<String, BurningManCandidate> getBurningManCandidatesByName(int chainHeight, boolean limitCappingRounds) {
Map<String, BurningManCandidate> burningManCandidatesByName = new TreeMap<>();
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();
BurningManCandidate candidate = burningManCandidatesByName.computeIfAbsent(name, n -> new BurningManCandidate());

// Issuance
Optional<String> customAddress = compensationProposal.getBurningManReceiverAddress();
boolean isCustomAddress = customAddress.isPresent();
Optional<String> receiverAddress;
if (isCustomAddress) {
receiverAddress = customAddress;
} else {
// We take change address from compensation request
receiverAddress = daoStateService.getTx(compensationProposal.getTxId())
.map(this::getAddressFromCompensationRequest);
}
if (receiverAddress.isPresent()) {
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(receiverAddress.get(),
isCustomAddress,
issuanceAmount,
decayedIssuanceAmount,
issuanceHeight,
issuance.getTxId(),
issuanceDate,
cycleIndex));
}
}
addBurnOutputModel(chainHeight, proofOfBurnOpReturnTxOutputByHash, name, candidate);
});
}
);
forEachCompensationIssuance(chainHeight, (issuance, compensationProposal) -> {
String name = compensationProposal.getName();
BurningManCandidate candidate = burningManCandidatesByName.computeIfAbsent(name, n -> new BurningManCandidate());

// Issuance
Optional<String> customAddress = compensationProposal.getBurningManReceiverAddress();
boolean isCustomAddress = customAddress.isPresent();
Optional<String> receiverAddress;
if (isCustomAddress) {
receiverAddress = customAddress;
} else {
// We take change address from compensation request
receiverAddress = daoStateService.getTx(compensationProposal.getTxId())
.map(this::getAddressFromCompensationRequest);
}
if (receiverAddress.isPresent()) {
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(receiverAddress.get(),
isCustomAddress,
issuanceAmount,
decayedIssuanceAmount,
issuanceHeight,
issuance.getTxId(),
issuanceDate,
cycleIndex));
}
}
addBurnOutputModel(chainHeight, proofOfBurnOpReturnTxOutputByHash, name, candidate);
});

// Add output receivers of genesis transaction
daoStateService.getGenesisTx()
Expand Down Expand Up @@ -209,52 +205,21 @@ Map<String, BurningManCandidate> getBurningManCandidatesByName(int chainHeight,
return burningManCandidatesByName;
}

private static int imposeCaps(Collection<BurningManCandidate> burningManCandidates, boolean limitCappingRounds) {
List<BurningManCandidate> candidatesInDescendingBurnCapRatio = new ArrayList<>(burningManCandidates);
candidatesInDescendingBurnCapRatio.sort(Comparator.comparing(BurningManCandidate::getBurnCapRatio).reversed());
double thresholdBurnCapRatio = 1.0;
double remainingBurnShare = 1.0;
double remainingCapShare = 1.0;
int cappingRound = 0;
for (BurningManCandidate candidate : candidatesInDescendingBurnCapRatio) {
double invScaleFactor = remainingBurnShare / remainingCapShare;
double burnCapRatio = candidate.getBurnCapRatio();
if (remainingCapShare <= 0.0 || burnCapRatio <= 0.0 || burnCapRatio < invScaleFactor ||
limitCappingRounds && burnCapRatio < 1.0) {
cappingRound++;
break;
}
if (burnCapRatio < thresholdBurnCapRatio) {
thresholdBurnCapRatio = invScaleFactor;
cappingRound++;
}
candidate.imposeCap(cappingRound, candidate.getBurnAmountShare() / thresholdBurnCapRatio);
remainingBurnShare -= candidate.getBurnAmountShare();
remainingCapShare -= candidate.getMaxBoostedCompensationShare();
}
return cappingRound;
}

String getLegacyBurningManAddress(int chainHeight) {
return daoStateService.getParamValue(Param.RECIPIENT_BTC_ADDRESS, chainHeight);
}

Set<BurningManCandidate> getActiveBurningManCandidates(int chainHeight) {
return getActiveBurningManCandidates(chainHeight, !DelayedPayoutTxReceiverService.isProposal412Activated());
List<BurningManCandidate> getActiveBurningManCandidates(int chainHeight) {
return getActiveBurningManCandidates(chainHeight, false);
}

Set<BurningManCandidate> getActiveBurningManCandidates(int chainHeight, boolean limitCappingRounds) {
List<BurningManCandidate> getActiveBurningManCandidates(int chainHeight, boolean limitCappingRounds) {
return getBurningManCandidatesByName(chainHeight, limitCappingRounds).values().stream()
.filter(burningManCandidate -> burningManCandidate.getCappedBurnAmountShare() > 0)
.filter(candidate -> candidate.getReceiverAddress().isPresent())
.collect(Collectors.toSet());
.filter(BurningManCandidate::isReceiverAddressValid)
.collect(Collectors.toList());
}


///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////

Map<P2PDataStorage.ByteArray, Set<TxOutput>> getProofOfBurnOpReturnTxOutputByHash(int chainHeight) {
Map<P2PDataStorage.ByteArray, Set<TxOutput>> map = new HashMap<>();
daoStateService.getProofOfBurnOpReturnTxOutputs().stream()
Expand All @@ -266,15 +231,22 @@ Map<P2PDataStorage.ByteArray, Set<TxOutput>> getProofOfBurnOpReturnTxOutputByHas
return map;
}

private Stream<CompensationProposal> getCompensationProposalsForIssuance(Issuance issuance) {
return proposalService.getProposalPayloads().stream()

///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////

private void forEachCompensationIssuance(int chainHeight, BiConsumer<Issuance, CompensationProposal> action) {
proposalService.getProposalPayloads().stream()
.map(ProposalPayload::getProposal)
.filter(proposal -> issuance.getTxId().equals(proposal.getTxId()))
.filter(proposal -> proposal instanceof CompensationProposal)
.map(proposal -> (CompensationProposal) proposal);
.flatMap(proposal -> daoStateService.getIssuance(proposal.getTxId())
.filter(issuance -> issuance.getChainHeight() <= chainHeight)
.map(issuance -> new Tuple2<>(issuance, (CompensationProposal) proposal))
.stream())
.forEach(pair -> action.accept(pair.first, pair.second));
}


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.
Expand Down Expand Up @@ -380,4 +352,30 @@ private long getDecayedBurnedAmount(long amount, int issuanceHeight, int chainHe
private long getDecayedGenesisOutputAmount(long amount) {
return Math.round(amount * GENESIS_OUTPUT_AMOUNT_FACTOR);
}

private static int imposeCaps(Collection<BurningManCandidate> burningManCandidates, boolean limitCappingRounds) {
List<BurningManCandidate> candidatesInDescendingBurnCapRatio = new ArrayList<>(burningManCandidates);
candidatesInDescendingBurnCapRatio.sort(Comparator.comparing(BurningManCandidate::getBurnCapRatio).reversed());
double thresholdBurnCapRatio = 1.0;
double remainingBurnShare = 1.0;
double remainingCapShare = 1.0;
int cappingRound = 0;
for (BurningManCandidate candidate : candidatesInDescendingBurnCapRatio) {
double invScaleFactor = remainingBurnShare / remainingCapShare;
double burnCapRatio = candidate.getBurnCapRatio();
if (remainingCapShare <= 0.0 || burnCapRatio <= 0.0 || burnCapRatio < invScaleFactor ||
limitCappingRounds && burnCapRatio < 1.0) {
cappingRound++;
break;
}
if (burnCapRatio < thresholdBurnCapRatio) {
thresholdBurnCapRatio = invScaleFactor;
cappingRound++;
}
candidate.imposeCap(cappingRound, candidate.getBurnAmountShare() / thresholdBurnCapRatio);
remainingBurnShare -= candidate.getBurnAmountShare();
remainingCapShare -= candidate.getMaxBoostedCompensationShare();
}
return cappingRound;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,6 @@ public class DelayedPayoutTxReceiverService implements DaoStateListener {
// See: https://github.com/bisq-network/proposals/issues/412
public static final Date PROPOSAL_412_ACTIVATION_DATE = Utilities.getUTCDate(2024, GregorianCalendar.MAY, 1);

public static boolean isBugfix6699Activated() {
return new Date().after(BUGFIX_6699_ACTIVATION_DATE);
}

@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean isProposal412Activated() {
return new Date().after(PROPOSAL_412_ACTIVATION_DATE);
}

// We don't allow to get further back than 767950 (the block height from Dec. 18th 2022).
static final int MIN_SNAPSHOT_HEIGHT = Config.baseCurrencyNetwork().isRegtest() ? 0 : 767950;

Expand Down Expand Up @@ -131,15 +122,17 @@ public int getBurningManSelectionHeight() {
public List<Tuple2<Long, String>> getReceivers(int burningManSelectionHeight,
long inputAmount,
long tradeTxFee) {
return getReceivers(burningManSelectionHeight, inputAmount, tradeTxFee, isBugfix6699Activated());
return getReceivers(burningManSelectionHeight, inputAmount, tradeTxFee, true, true);
}

public List<Tuple2<Long, String>> getReceivers(int burningManSelectionHeight,
long inputAmount,
long tradeTxFee,
boolean isBugfix6699Activated) {
boolean isBugfix6699Activated,
boolean isProposal412Activated) {
checkArgument(burningManSelectionHeight >= MIN_SNAPSHOT_HEIGHT, "Selection height must be >= " + MIN_SNAPSHOT_HEIGHT);
Collection<BurningManCandidate> burningManCandidates = burningManService.getActiveBurningManCandidates(burningManSelectionHeight);
Collection<BurningManCandidate> burningManCandidates = burningManService.getActiveBurningManCandidates(burningManSelectionHeight,
!isProposal412Activated);

// 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.
Expand All @@ -162,8 +155,8 @@ public List<Tuple2<Long, String>> getReceivers(int burningManSelectionHeight,
}

long spendableAmount = getSpendableAmount(burningManCandidates.size(), inputAmount, txFeePerVbyte);
// We only use outputs > 1000 sat or at least 2 times the cost for the output (32 bytes).
// If we remove outputs it will be spent as miner fee.
// 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 distributed to the remaining receivers.
long minOutputAmount = Math.max(DPT_MIN_OUTPUT_AMOUNT, txFeePerVbyte * 32 * 2);
// Sanity check that max share of a non-legacy BM is 20% over MAX_BURN_SHARE (taking into account potential increase due adjustment)
long maxOutputAmount = Math.round(spendableAmount * (BurningManService.MAX_BURN_SHARE * 1.2));
Expand All @@ -178,6 +171,9 @@ public List<Tuple2<Long, String>> getReceivers(int burningManSelectionHeight,
})
.sum();

// FIXME: The small outputs should be filtered out before adjustment, not afterwards. Otherwise, outputs of
// amount just under 1000 sats or 64 * fee-rate could get erroneously included and lead to significant
// underpaying of the DPT (by perhaps around 5-10% per erroneously included output).
List<Tuple2<Long, String>> receivers = burningManCandidates.stream()
.filter(candidate -> candidate.getReceiverAddress(isBugfix6699Activated).isPresent())
.map(candidate -> {
Expand Down
Loading

0 comments on commit b8e3296

Please sign in to comment.