-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Validate BSQ fee payment using DAO tx info. #6425
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ | |
|
||
import bisq.core.dao.governance.param.Param; | ||
import bisq.core.dao.state.DaoStateService; | ||
import bisq.core.dao.state.model.blockchain.Tx; | ||
import bisq.core.filter.FilterManager; | ||
|
||
import bisq.common.util.Tuple2; | ||
|
@@ -56,12 +57,11 @@ public class TxValidator { | |
|
||
private final DaoStateService daoStateService; | ||
private final FilterManager filterManager; | ||
private long blockHeightAtOfferCreation; // Only set for maker. | ||
private long feePaymentBlockHeight; // applicable to maker and taker fees | ||
private final List<String> errorList; | ||
private final String txId; | ||
private Coin amount; | ||
@Nullable | ||
private Boolean isFeeCurrencyBtc = null; | ||
private Boolean isFeeCurrencyBtc = true; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As isFeeCurrencyBtc is only used by one constructor and otherwise is not set (and not used) I think keeping it a nullable Boolean is better. In the use cases where its accessed the constructor is use with a non null value, so no need to initialize it with true. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is one unused constructor as well. Better to remove that. |
||
@Nullable | ||
private Long chainHeight; | ||
@Setter | ||
|
@@ -85,13 +85,13 @@ public TxValidator(DaoStateService daoStateService, | |
String txId, | ||
Coin amount, | ||
@Nullable Boolean isFeeCurrencyBtc, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Better remove the @nullable and use primitive boolean value as the callers always provide a non null value. |
||
long blockHeightAtOfferCreation, | ||
long feePaymentBlockHeight, | ||
FilterManager filterManager) { | ||
this.daoStateService = daoStateService; | ||
this.txId = txId; | ||
this.amount = amount; | ||
this.isFeeCurrencyBtc = isFeeCurrencyBtc; | ||
this.blockHeightAtOfferCreation = blockHeightAtOfferCreation; | ||
this.feePaymentBlockHeight = feePaymentBlockHeight; | ||
this.filterManager = filterManager; | ||
this.errorList = new ArrayList<>(); | ||
this.jsonTxt = ""; | ||
|
@@ -119,8 +119,6 @@ public TxValidator parseJsonValidateMakerFeeTx(String jsonTxt, List<String> btcF | |
if (checkNotNull(isFeeCurrencyBtc)) { | ||
status = checkFeeAddressBTC(jsonTxt, btcFeeReceivers) | ||
&& checkFeeAmountBTC(jsonTxt, amount, true, getBlockHeightForFeeCalculation(jsonTxt)); | ||
} else { | ||
status = checkFeeAmountBSQ(jsonTxt, amount, true, getBlockHeightForFeeCalculation(jsonTxt)); | ||
} | ||
} | ||
} catch (JsonSyntaxException e) { | ||
|
@@ -132,6 +130,17 @@ public TxValidator parseJsonValidateMakerFeeTx(String jsonTxt, List<String> btcF | |
return endResult("Maker tx validation", status); | ||
} | ||
|
||
public TxValidator validateBsqFeeTx(boolean isMaker) { | ||
Optional<Tx> tx = daoStateService.getTx(txId); | ||
String statusStr = isMaker ? "Maker" : "Taker" + " tx validation"; | ||
if (tx.isEmpty()) { | ||
log.info("DAO does not yet have the tx {}, bypassing check of burnt BSQ amount.", txId); | ||
return endResult(statusStr, true); | ||
} else { | ||
return endResult(statusStr, checkFeeAmountBSQ(tx.get(), amount, isMaker, feePaymentBlockHeight)); | ||
} | ||
} | ||
|
||
public TxValidator parseJsonValidateTakerFeeTx(String jsonTxt, List<String> btcFeeReceivers) { | ||
this.jsonTxt = jsonTxt; | ||
boolean status = initialSanityChecks(txId, jsonTxt); | ||
|
@@ -143,8 +152,6 @@ public TxValidator parseJsonValidateTakerFeeTx(String jsonTxt, List<String> btcF | |
if (isFeeCurrencyBtc) { | ||
status = checkFeeAddressBTC(jsonTxt, btcFeeReceivers) | ||
&& checkFeeAmountBTC(jsonTxt, amount, false, getBlockHeightForFeeCalculation(jsonTxt)); | ||
} else { | ||
status = checkFeeAmountBSQ(jsonTxt, amount, false, getBlockHeightForFeeCalculation(jsonTxt)); | ||
} | ||
} | ||
} catch (JsonSyntaxException e) { | ||
|
@@ -250,36 +257,24 @@ private boolean checkFeeAmountBTC(String jsonTxt, Coin tradeAmount, boolean isMa | |
return false; | ||
} | ||
|
||
// I think its better to postpone BSQ fee check once the BSQ trade fee tx is confirmed and then use the BSQ explorer to request the | ||
// BSQ fee to check if it is correct. | ||
// Otherwise the requirements here become very complicated and potentially impossible to verify as we don't know | ||
// if inputs and outputs are valid BSQ without the BSQ parser and confirmed transactions. | ||
private boolean checkFeeAmountBSQ(String jsonTxt, Coin tradeAmount, boolean isMaker, long blockHeight) { | ||
JsonArray jsonVin = getVinAndVout(jsonTxt).first; | ||
JsonArray jsonVout = getVinAndVout(jsonTxt).second; | ||
JsonObject jsonVin0 = jsonVin.get(0).getAsJsonObject(); | ||
JsonObject jsonVout0 = jsonVout.get(0).getAsJsonObject(); | ||
JsonElement jsonVIn0Value = jsonVin0.getAsJsonObject("prevout").get("value"); | ||
JsonElement jsonVOut0Value = jsonVout0.getAsJsonObject().get("value"); | ||
if (jsonVIn0Value == null || jsonVOut0Value == null) { | ||
throw new JsonSyntaxException("vin/vout missing data"); | ||
} | ||
private boolean checkFeeAmountBSQ(Tx bsqTx, Coin tradeAmount, boolean isMaker, long blockHeight) { | ||
Param minFeeParam = isMaker ? Param.MIN_MAKER_FEE_BSQ : Param.MIN_TAKER_FEE_BSQ; | ||
long expectedFeeAsLong = calculateFee(tradeAmount, | ||
isMaker ? getMakerFeeRateBsq(blockHeight) : getTakerFeeRateBsq(blockHeight), | ||
minFeeParam).getValue(); | ||
long feeValue = getBsqBurnt(jsonVin, jsonVOut0Value.getAsLong(), expectedFeeAsLong); | ||
|
||
long feeValue = bsqTx.getBurntBsq(); | ||
log.debug("BURNT BSQ maker fee: {} BSQ ({} sats)", (double) feeValue / 100.0, feeValue); | ||
String description = String.format("Expected fee: %.2f BSQ, actual fee paid: %.2f BSQ", | ||
(double) expectedFeeAsLong / 100.0, (double) feeValue / 100.0); | ||
String description = String.format("Expected fee: %.2f BSQ, actual fee paid: %.2f BSQ, Trade amount: %s", | ||
(double) expectedFeeAsLong / 100.0, (double) feeValue / 100.0, tradeAmount.toPlainString()); | ||
|
||
if (expectedFeeAsLong == feeValue) { | ||
log.debug("The fee matched. " + description); | ||
return true; | ||
} | ||
|
||
if (expectedFeeAsLong < feeValue) { | ||
log.info("The fee was more than what we expected. " + description); | ||
log.info("The fee was more than what we expected. " + description + " Tx:" + bsqTx.getId()); | ||
return true; | ||
} | ||
|
||
|
@@ -350,39 +345,6 @@ private static boolean initialSanityChecks(String txId, String jsonTxt) { | |
// we don't care if it is confirmed or not, just that it exists. | ||
} | ||
|
||
// a BSQ maker/taker fee transaction looks like this: | ||
// BSQ INPUT 1 BSQ OUTPUT | ||
// BSQ INPUT 2 BTC OUTPUT FOR RESERVED AMOUNT | ||
// BSQ INPUT n BTC OUTPUT FOR CHANGE | ||
// BTC INPUT 1 | ||
// BTC INPUT 2 | ||
// BTC INPUT n | ||
// there can be any number of BSQ inputs and BTC inputs | ||
// BSQ inputs always come first in the tx, followed by BTC for the collateral. | ||
// the sum of all BSQ inputs minus the BSQ output is the burnt amount, or trading fee. | ||
long getBsqBurnt(JsonArray jsonVin, long bsqOutValue, long expectedFee) { | ||
// sum consecutive inputs until we have accumulated enough to cover the output + burnt amount | ||
long bsqInValue = 0; | ||
for (int txIndex = 0; txIndex < jsonVin.size() - 1; txIndex++) { | ||
bsqInValue += jsonVin.get(txIndex).getAsJsonObject().getAsJsonObject("prevout").get("value").getAsLong(); | ||
if (bsqInValue - expectedFee >= bsqOutValue) { | ||
break; // target reached - bsq input exceeds the output and expected burn amount | ||
} | ||
} | ||
// guard against negative burn amount (i.e. only 1 tx input, or first in < first out) | ||
long burntAmount = Math.max(0, bsqInValue - bsqOutValue); | ||
// since we do not know which of the first 'n' are definitively BSQ inputs, sanity-check that the burnt amount | ||
// is not too ridiculously high, as that would imply that we counted a BTC input. | ||
if (burntAmount > 10 * expectedFee) { | ||
log.error("The apparent BSQ fee burnt seems ridiculously high ({}) compared to expected ({})", burntAmount, expectedFee); | ||
burntAmount = 0; // returning zero will flag the trade for manual review | ||
} | ||
if (burntAmount == 0) { | ||
log.error("Could not obtain the burnt BSQ amount, trade will be flagged for manual review."); | ||
} | ||
return burntAmount; | ||
} | ||
|
||
private static long getTxConfirms(String jsonTxt, long chainHeight) { | ||
long blockHeight = getTxBlockHeight(jsonTxt); | ||
if (blockHeight > 0) { | ||
|
@@ -395,8 +357,8 @@ private static long getTxConfirms(String jsonTxt, long chainHeight) { | |
// if the tx is not yet confirmed, use current block tip, if tx is confirmed use the block it was confirmed at. | ||
private long getBlockHeightForFeeCalculation(String jsonTxt) { | ||
// For the maker we set the blockHeightAtOfferCreation from the offer | ||
if (blockHeightAtOfferCreation > 0) { | ||
return blockHeightAtOfferCreation; | ||
if (feePaymentBlockHeight > 0) { | ||
return feePaymentBlockHeight; | ||
} | ||
|
||
long txBlockHeight = getTxBlockHeight(jsonTxt); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This BSQ fee check should be gated by a call to
isServiceSupported()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need it there. The
MempoolService
is not used to validate the BSQ fee becausetxValidator.validateBsqFeeTx(true)
gets the transaction from theDaoStateService
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the change to have BSQ fees checked using the DAO database rather than with data obtained from mempool.space,
MempoolService
is incorrectly named. (In fact, it was unfortunate that it was named referring to an implementation feature rather than functionality).It would be more appropriately called
TradeFeeCheckingService
. Fee checking should be gated byisServiceSupported()
, so that the filter and config settings can do their job in enabling/disabling the fee checking feature.