Merge pull request #6632 from HenrikJannsen/improve_validations

Add more validations
This commit is contained in:
Alejandro García 2023-04-04 06:52:47 +00:00 committed by GitHub
commit f49be9ab3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 198 additions and 20 deletions

View File

@ -19,4 +19,6 @@ package bisq.core.trade.txproof;
public interface AssetTxProofParser<R extends AssetTxProofRequest.Result, T extends AssetTxProofModel> { public interface AssetTxProofParser<R extends AssetTxProofRequest.Result, T extends AssetTxProofModel> {
R parse(T model, String jsonTxt); R parse(T model, String jsonTxt);
R parse(String jsonTxt);
} }

View File

@ -0,0 +1,84 @@
/*
* 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.trade.txproof.xmr;
import bisq.core.trade.txproof.AssetTxProofParser;
import bisq.common.app.DevEnv;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class XmrRawTxParser implements AssetTxProofParser<XmrTxProofRequest.Result, XmrTxProofModel> {
XmrRawTxParser() {
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public XmrTxProofRequest.Result parse(XmrTxProofModel model, String jsonTxt) {
return parse(jsonTxt);
}
@SuppressWarnings("SpellCheckingInspection")
@Override
public XmrTxProofRequest.Result parse(String jsonTxt) {
try {
JsonObject json = new Gson().fromJson(jsonTxt, JsonObject.class);
if (json == null) {
return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Empty json"));
}
// there should always be "data" and "status" at the top level
if (json.get("data") == null || !json.get("data").isJsonObject() || json.get("status") == null) {
return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Missing data / status fields"));
}
JsonObject jsonData = json.get("data").getAsJsonObject();
String jsonStatus = json.get("status").getAsString();
if (jsonStatus.matches("fail")) {
// The API returns "fail" until the transaction has successfully reached the mempool or if request
// contained invalid data.
// We return TX_NOT_FOUND which will cause a retry later
return XmrTxProofRequest.Result.PENDING.with(XmrTxProofRequest.Detail.TX_NOT_FOUND);
} else if (!jsonStatus.matches("success")) {
return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Unhandled status value"));
}
JsonElement jsonUnlockTime = jsonData.get("unlock_time");
if (jsonUnlockTime == null) {
return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Missing unlock_time field"));
} else {
long unlockTime = jsonUnlockTime.getAsLong();
if (unlockTime != 0 && !DevEnv.isDevMode()) {
log.warn("Invalid unlock_time {}", unlockTime);
return XmrTxProofRequest.Result.FAILED.with(XmrTxProofRequest.Detail.INVALID_UNLOCK_TIME.error("Invalid unlock_time"));
}
}
return XmrTxProofRequest.Result.SUCCESS.with(XmrTxProofRequest.Detail.SUCCESS);
} catch (JsonParseException | NullPointerException e) {
return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error(e.toString()));
}
}
}

View File

@ -45,6 +45,11 @@ public class XmrTxProofParser implements AssetTxProofParser<XmrTxProofRequest.Re
// API // API
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@Override
public XmrTxProofRequest.Result parse(String jsonTxt) {
throw new UnsupportedOperationException("This method is not supported for this parser");
}
@SuppressWarnings("SpellCheckingInspection") @SuppressWarnings("SpellCheckingInspection")
@Override @Override
public XmrTxProofRequest.Result parse(XmrTxProofModel model, String jsonTxt) { public XmrTxProofRequest.Result parse(XmrTxProofModel model, String jsonTxt) {

View File

@ -37,6 +37,8 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import java.io.IOException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -101,6 +103,7 @@ class XmrTxProofRequest implements AssetTxProofRequest<XmrTxProofRequest.Result>
NO_MATCH_FOUND, NO_MATCH_FOUND,
AMOUNT_NOT_MATCHING, AMOUNT_NOT_MATCHING,
TRADE_DATE_NOT_MATCHING, TRADE_DATE_NOT_MATCHING,
INVALID_UNLOCK_TIME,
NO_RESULTS_TIMEOUT; NO_RESULTS_TIMEOUT;
@Getter @Getter
@ -143,7 +146,8 @@ class XmrTxProofRequest implements AssetTxProofRequest<XmrTxProofRequest.Result>
private final ListeningExecutorService executorService = Utilities.getListeningExecutorService( private final ListeningExecutorService executorService = Utilities.getListeningExecutorService(
"XmrTransferProofRequester", 3, 5, 10 * 60); "XmrTransferProofRequester", 3, 5, 10 * 60);
private final AssetTxProofParser<XmrTxProofRequest.Result, XmrTxProofModel> parser; private final AssetTxProofParser<XmrTxProofRequest.Result, XmrTxProofModel> txProofParser;
private final AssetTxProofParser<XmrTxProofRequest.Result, XmrTxProofModel> rawTxParser;
private final XmrTxProofModel model; private final XmrTxProofModel model;
private final AssetTxProofHttpClient httpClient; private final AssetTxProofHttpClient httpClient;
private final long firstRequest; private final long firstRequest;
@ -160,7 +164,8 @@ class XmrTxProofRequest implements AssetTxProofRequest<XmrTxProofRequest.Result>
XmrTxProofRequest(Socks5ProxyProvider socks5ProxyProvider, XmrTxProofRequest(Socks5ProxyProvider socks5ProxyProvider,
XmrTxProofModel model) { XmrTxProofModel model) {
this.parser = new XmrTxProofParser(); txProofParser = new XmrTxProofParser();
rawTxParser = new XmrRawTxParser();
this.model = model; this.model = model;
httpClient = new XmrTxProofHttpClient(socks5ProxyProvider); httpClient = new XmrTxProofHttpClient(socks5ProxyProvider);
@ -206,24 +211,20 @@ class XmrTxProofRequest implements AssetTxProofRequest<XmrTxProofRequest.Result>
ListenableFuture<Result> future = executorService.submit(() -> { ListenableFuture<Result> future = executorService.submit(() -> {
Thread.currentThread().setName("XmrTransferProofRequest-" + this.getShortId()); Thread.currentThread().setName("XmrTransferProofRequest-" + this.getShortId());
// The API use the viewkey param for txKey if txprove is true
// https://github.com/moneroexamples/onion-monero-blockchain-explorer/blob/9a37839f37abef0b8b94ceeba41ab51a41f3fbd8/src/page.h#L5254 Result result = getResultFromRawTxRequest();
String param = "/api/outputs?txhash=" + model.getTxHash() + if (result != Result.SUCCESS) {
"&address=" + model.getRecipientAddress() + return result;
"&viewkey=" + model.getTxKey() +
"&txprove=1";
log.info("Param {} for {}", param, this);
String json = httpClient.get(param, "User-Agent", "bisq/" + Version.VERSION);
try {
String prettyJson = new GsonBuilder().setPrettyPrinting().create().toJson(new JsonParser().parse(json));
log.info("Response json from {}\n{}", this, prettyJson);
} catch (Throwable error) {
log.error("Pretty print caused a {}: raw json={}", error, json);
} }
Result result = parser.parse(model, json); if (terminated) {
log.info("Result from {}\n{}", this, result); return null;
return result; }
// Only if the rawTx request succeeded we go on to the tx proof request.
// The result from the rawTx request does not contain any detail data in the
// success case, so we drop it.
return getResultFromTxProofRequest();
}); });
Futures.addCallback(future, new FutureCallback<>() { Futures.addCallback(future, new FutureCallback<>() {
@ -265,7 +266,7 @@ class XmrTxProofRequest implements AssetTxProofRequest<XmrTxProofRequest.Result>
} }
public void onFailure(@NotNull Throwable throwable) { public void onFailure(@NotNull Throwable throwable) {
String errorMessage = this + " failed with error " + throwable.toString(); String errorMessage = this + " failed with error " + throwable;
faultHandler.handleFault(errorMessage, throwable); faultHandler.handleFault(errorMessage, throwable);
UserThread.execute(() -> UserThread.execute(() ->
resultHandler.accept(XmrTxProofRequest.Result.ERROR.with(Detail.CONNECTION_FAILURE.error(errorMessage)))); resultHandler.accept(XmrTxProofRequest.Result.ERROR.with(Detail.CONNECTION_FAILURE.error(errorMessage))));
@ -273,6 +274,45 @@ class XmrTxProofRequest implements AssetTxProofRequest<XmrTxProofRequest.Result>
}, MoreExecutors.directExecutor()); }, MoreExecutors.directExecutor());
} }
private Result getResultFromRawTxRequest() throws IOException {
// The rawtransaction endpoint is not documented in explorer docs.
// Example request: https://xmrblocks.bisq.services/api/rawtransaction/5e665addf6d7c6300670e8a89564ed12b5c1a21c336408e2835668f9a6a0d802
String param = "/api/rawtransaction/" + model.getTxHash();
log.info("Param {} for rawtransaction request {}", param, this);
String json = httpClient.get(param, "User-Agent", "bisq/" + Version.VERSION);
try {
String prettyJson = new GsonBuilder().setPrettyPrinting().create().toJson(new JsonParser().parse(json));
log.info("Response json from rawtransaction request {}\n{}", this, prettyJson);
} catch (Throwable error) {
log.error("Pretty print caused a {}: raw json={}", error, json);
}
Result result = rawTxParser.parse(json);
log.info("Result from rawtransaction request {}\n{}", this, result);
return result;
}
private Result getResultFromTxProofRequest() throws IOException {
// The API use the viewkey param for txKey if txprove is true
// https://github.com/moneroexamples/onion-monero-blockchain-explorer/blob/9a37839f37abef0b8b94ceeba41ab51a41f3fbd8/src/page.h#L5254
String param = "/api/outputs?txhash=" + model.getTxHash() +
"&address=" + model.getRecipientAddress() +
"&viewkey=" + model.getTxKey() +
"&txprove=1";
log.info("Param {} for {}", param, this);
String json = httpClient.get(param, "User-Agent", "bisq/" + Version.VERSION);
try {
String prettyJson = new GsonBuilder().setPrettyPrinting().create().toJson(new JsonParser().parse(json));
log.info("Response json from {}\n{}", this, prettyJson);
} catch (Throwable error) {
log.error("Pretty print caused a {}: raw json={}", error, json);
}
Result result = txProofParser.parse(model, json);
log.info("Result from {}\n{}", this, result);
return result;
}
@Override @Override
public void terminate() { public void terminate() {
executorService.shutdown(); executorService.shutdown();

View File

@ -156,7 +156,7 @@ class XmrTxProofRequestsPerTrade implements AssetTxProofRequestsPerTrade {
request.requestFromService(result -> { request.requestFromService(result -> {
// If we ever received an error or failed result we terminate and do not process any // If we ever received an error or failed result we terminate and do not process any
// future result anymore to avoid that we overwrite out state with success. // future result anymore to avoid that we overwrite our state with success.
if (wasTerminated()) { if (wasTerminated()) {
return; return;
} }

View File

@ -0,0 +1,47 @@
package bisq.core.trade.txproof.xmr;
import org.junit.Test;
import static org.junit.Assert.assertSame;
public class XmrRawTxParserTest {
private final XmrRawTxParser parser = new XmrRawTxParser();
@Test
public void testJsonRoot() {
// checking what happens when bad input is provided
assertSame(parser.parse("invalid json data").getDetail(), XmrTxProofRequest.Detail.API_INVALID);
assertSame(parser.parse("").getDetail(), XmrTxProofRequest.Detail.API_INVALID);
assertSame(parser.parse("[]").getDetail(), XmrTxProofRequest.Detail.API_INVALID);
assertSame(parser.parse("{}").getDetail(), XmrTxProofRequest.Detail.API_INVALID);
}
@Test
public void testJsonTopLevel() {
// testing the top level fields: data and status
assertSame(parser.parse("{'data':{'title':''},'status':'fail'}")
.getDetail(), XmrTxProofRequest.Detail.TX_NOT_FOUND);
assertSame(parser.parse("{'data':{'title':''},'missingstatus':'success'}")
.getDetail(), XmrTxProofRequest.Detail.API_INVALID);
assertSame(parser.parse("{'missingdata':{'title':''},'status':'success'}")
.getDetail(), XmrTxProofRequest.Detail.API_INVALID);
}
@Test
public void testJsonTxUnlockTime() {
String missing_tx_timestamp = "{'data':{'version':'2'}, 'status':'success'}";
assertSame(parser.parse(missing_tx_timestamp).getDetail(), XmrTxProofRequest.Detail.API_INVALID);
String invalid_unlock_time = "{'data':{'unlock_time':'1'}, 'status':'success'}";
assertSame(parser.parse(invalid_unlock_time).getDetail(), XmrTxProofRequest.Detail.INVALID_UNLOCK_TIME);
String valid_unlock_time = "{'data':{'unlock_time':'0'}, 'status':'success'}";
assertSame(parser.parse(valid_unlock_time).getDetail(), XmrTxProofRequest.Detail.SUCCESS);
}
@Test
public void testJsonFail() {
String failedJson = "{\"data\":null,\"message\":\"Cant parse tx hash: a\",\"status\":\"error\"}";
assertSame(parser.parse(failedJson).getDetail(), XmrTxProofRequest.Detail.API_INVALID);
}
}