Skip to content

Commit

Permalink
Merge pull request #92 from jmacxx/entitlement_checking_mempool
Browse files Browse the repository at this point in the history
Check roles using mempool tx lookup.
  • Loading branch information
chimp1984 authored Feb 10, 2022
2 parents 00ddca7 + 66a2320 commit 3f4bfa3
Show file tree
Hide file tree
Showing 11 changed files with 367 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ public DefaultApplicationService(String[] args) {

accountService = new AccountService(persistenceService);

userProfileService = new UserProfileService(persistenceService, keyPairService, identityService, networkService);
UserProfileService.Config userProfileServiceConfig = UserProfileService.Config.from(getConfig("bisq.userProfileServiceConfig"));
userProfileService = new UserProfileService(persistenceService, userProfileServiceConfig, keyPairService, identityService, networkService);
chatService = new ChatService(persistenceService, identityService, networkService,userProfileService);
tradeIntentListingsService = new TradeIntentListingsService(networkService);
tradeIntentService = new TradeIntentService(networkService, identityService, tradeIntentListingsService, chatService);
Expand Down
13 changes: 13 additions & 0 deletions application/src/main/resources/Bisq.conf
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ bisq {
minPoolSize = 5
}

userProfileServiceConfig = {
btcMempoolProviders = [
"https://mempool.emzy.de/api/",
"https://mempool.space/api/",
"https://markets.bisq.services/api/"
]
bsqMempoolProviders = [
"https://bisq.mempool.emzy.de/bisq/api/",
"https://bisq.markets/bisq/api/",
"https://markets.bisq.services/bisq/api/"
]
}

networkServiceConfig = {
// supportedTransportTypes = ["CLEAR", "TOR", "I2P"]
supportedTransportTypes = ["CLEAR"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,10 @@ private CompletableFuture<Optional<Entitlement.Proof>> onVerifyProofOfBurn(Entit
});
}

private CompletableFuture<Optional<Entitlement.Proof>> onVerifyBondedRole(EntitlementItem entitlementItem, String bondedRoleTxId, String bondedRoleSig) {
private CompletableFuture<Optional<Entitlement.Proof>> onVerifyBondedRole(EntitlementItem entitlementItem, String bondedRoleTxId, String pubKeyHash, String bondedRoleSig) {
return userProfileService.verifyBondedRole(bondedRoleTxId,
bondedRoleSig,
model.keyPair.get().getPublic())
pubKeyHash)
.whenComplete((proof, throwable) -> {
UIThread.run(() -> {
if (throwable == null) {
Expand Down Expand Up @@ -180,6 +180,9 @@ private static class Model implements bisq.desktop.common.view.Model {
private final BooleanProperty tableVisible = new SimpleBooleanProperty();
private final ObjectProperty<KeyPair> keyPair;
private String minBurnAmount;
private String getPubKeyHash() {
return Hex.encode(DigestUtil.hash(keyPair.get().getPublic().getEncoded()));
}

private Model(ObjectProperty<KeyPair> keyPair) {
this.keyPair = keyPair;
Expand Down Expand Up @@ -312,7 +315,7 @@ protected void addContent() {

GridPane.setMargin(messageLabel, new Insets(0, 0, 20, 0));

String pubKeyHash = Hex.encode(DigestUtil.hash(model.keyPair.get().getPublic().getEncoded()));
String pubKeyHash = model.getPubKeyHash();

gridPane.addTextFieldWithCopyIcon(Res.get("social.createUserProfile.entitlement.popup.pubKeyHash"), pubKeyHash);
gridPane.addTextFieldWithCopyIcon(Res.get("social.createUserProfile.entitlement.popup.minBurnAmount"), model.minBurnAmount);
Expand Down Expand Up @@ -346,7 +349,7 @@ protected void addContent() {
secondField = gridPane.addTextField(Res.get("social.createUserProfile.entitlement.popup.bondedRole.sig"), "");
onAction(() -> {
actionButton.setDisable(true); //todo add busy animation
controller.onVerifyBondedRole(entitlementItem, firstField.getText(), secondField.getText())
controller.onVerifyBondedRole(entitlementItem, firstField.getText(), model.getPubKeyHash(), secondField.getText())
.whenComplete((proof, throwable) -> {
UIThread.run(() -> {
if (throwable == null && proof.isPresent()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
@Slf4j
public abstract class BaseHttpClient implements HttpClient {
public final String baseUrl;
protected final String userAgent;
public final String userAgent;
protected final String uid;

public boolean hasPendingRequest;
Expand Down
16 changes: 16 additions & 0 deletions security/src/main/java/bisq/security/KeyGeneration.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@

package bisq.security;

import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.ECPointUtil;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.provider.asymmetric.ec.EC5Util;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.math.ec.ECCurve;

import java.security.*;
import java.security.spec.ECGenParameterSpec;
Expand Down Expand Up @@ -47,6 +52,17 @@ public static PublicKey generatePublic(byte[] encodedKey) throws GeneralSecurity
return getKeyFactory().generatePublic(keySpec);
}

public static PublicKey generatePublicFromCompressed(byte[] compressedKey) throws GeneralSecurityException {
ECNamedCurveParameterSpec params = ECNamedCurveTable.getParameterSpec("secp256k1");
KeyFactory fact = KeyFactory.getInstance("ECDSA", "BC");
ECCurve curve = params.getCurve();
java.security.spec.EllipticCurve ellipticCurve = EC5Util.convertCurve(curve, params.getSeed());
java.security.spec.ECPoint point = ECPointUtil.decodePoint(ellipticCurve, compressedKey);
java.security.spec.ECParameterSpec params2 = EC5Util.convertSpec(ellipticCurve, params);
java.security.spec.ECPublicKeySpec keySpec = new java.security.spec.ECPublicKeySpec(point,params2);
return fact.generatePublic(keySpec);
}

public static PrivateKey generatePrivate(byte[] encodedKey) throws GeneralSecurityException {
EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encodedKey);
return getKeyFactory().generatePrivate(keySpec);
Expand Down
37 changes: 37 additions & 0 deletions security/src/main/java/bisq/security/SignatureUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@

package bisq.security;

import bisq.common.encoding.Hex;
import bisq.common.encoding.Base64;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.*;

public class SignatureUtil {
Expand All @@ -43,4 +49,35 @@ public static boolean verify(byte[] message, byte[] signature, PublicKey publicK
sig.update(message);
return sig.verify(signature);
}

// input: a base-64 bitcoin sig
// output a DER signature
public static byte [] bitcoinSigToDer(String bitcoinSig) {
String sigHex = Hex.encode(Base64.decode(bitcoinSig));
String r = Integer.parseInt(sigHex.substring(2, 4), 16 ) > 127 ?
"00" + sigHex.substring(2, 66) : sigHex.substring(2, 66);
String s = Integer.parseInt(sigHex.substring(66, 68), 16 ) > 127 ?
"00" + sigHex.substring(66) : sigHex.substring(66);
String result = "02" + String.format("%02X", r.length() / 2) + r +
"02" + String.format("%02X", s.length() / 2) + s;
result = "30" + String.format("%02X", result.length() / 2) + result;
return Hex.decode(result);
}

private static final String BITCOIN_SIGNED_MESSAGE_HEADER = "Bitcoin Signed Message:\n";
private static final byte[] BITCOIN_SIGNED_MESSAGE_HEADER_BYTES = BITCOIN_SIGNED_MESSAGE_HEADER.getBytes(StandardCharsets.UTF_8);

public static byte[] formatMessageForSigning(String message) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write(BITCOIN_SIGNED_MESSAGE_HEADER_BYTES.length);
bos.write(BITCOIN_SIGNED_MESSAGE_HEADER_BYTES);
byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);
bos.write(messageBytes.length);
bos.write(messageBytes);
return DigestUtil.sha256(DigestUtil.sha256(bos.toByteArray()));
} catch (IOException e) {
throw new RuntimeException(e); // Cannot happen.
}
}
}
1 change: 1 addition & 0 deletions social/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dependencies {
implementation project(':settings')

implementation 'com.github.chimp1984:jsocks'
implementation "com.google.code.gson:gson:2.8.5"
implementation 'com.google.guava:guava'
implementation 'com.typesafe:config'
}
Expand Down
2 changes: 1 addition & 1 deletion social/src/main/java/bisq/social/chat/ChatService.java
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ public CompletableFuture<Optional<PublicChannel>> addChannel(UserProfile userPro
.map(entitlement -> (Entitlement.BondedRoleProof) entitlement.proof())
.map(bondedRoleProof -> userProfileService.verifyBondedRole(bondedRoleProof.txId(),
bondedRoleProof.signature(),
userProfile.identity().pubKey().publicKey()))
userProfile.identity().id()))
.map(future -> future.thenApply(optionalProof -> optionalProof.map(e -> {
PublicChannel publicChannel = new PublicChannel(StringUtils.createUid(), channelName, userProfile);
persistableStore.getPublicChannels().add(publicChannel);
Expand Down
110 changes: 110 additions & 0 deletions social/src/main/java/bisq/social/userprofile/BsqTxValidator.java
Original file line number Diff line number Diff line change
@@ -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.social.userprofile;

import bisq.common.data.Pair;
import bisq.common.encoding.Hex;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;

import lombok.extern.slf4j.Slf4j;

import java.util.Optional;

@Slf4j
public class BsqTxValidator {

public static boolean initialSanityChecks(String txId, String jsonTxt) {
if (jsonTxt == null || jsonTxt.length() == 0) {
return false;
}
JsonObject json = new Gson().fromJson(jsonTxt, JsonObject.class);
// there should always be "id" string element at the top level
if (json.get("id") == null) {
return false;
}
// txid should match what we requested
if (!txId.equals(json.get("id").getAsString())) {
return false;
}
return true;
}

public static boolean isBsqTx(String url, String txId, String jsonTxt) {
return url.matches(".*bisq.*") && initialSanityChecks(txId, jsonTxt);
}

public static boolean isProofOfBurn(String jsonTxt) {
JsonObject json = new Gson().fromJson(jsonTxt, JsonObject.class);
JsonElement txType = json.get("txType");
if (txType == null) {
return false;
}
return txType.getAsString().equalsIgnoreCase("PROOF_OF_BURN");
}

public static boolean isLockup(String jsonTxt) {
JsonObject json = new Gson().fromJson(jsonTxt, JsonObject.class);
JsonElement txType = json.get("txType");
if (txType == null) {
return false;
}
return txType.getAsString().equalsIgnoreCase("LOCKUP");
}

public static long getBurntAmount(String jsonTxt) {
JsonObject json = new Gson().fromJson(jsonTxt, JsonObject.class);
JsonElement burntFee = json.get("burntFee");
if (burntFee == null) {
return 0; // no json element, assume zero burnt amount
}
return burntFee.getAsLong();
}

public static Optional<String> getOpReturnData(String jsonTxt) {
try {
Pair<JsonArray, JsonArray> vinAndVout = getVinAndVout(jsonTxt);
JsonArray voutArray = vinAndVout.second();
for (JsonElement x : voutArray) {
JsonObject y = x.getAsJsonObject();
if (y.get("txOutputType").getAsString().matches(".*OP_RETURN.*")) {
return Optional.of(y.get("opReturn").getAsString());
}
}
} catch (JsonSyntaxException e) {
log.error("json error:", e);
}
return Optional.empty();
}

private static Pair<JsonArray, JsonArray> getVinAndVout(String jsonTxt) throws JsonSyntaxException {
JsonObject json = new Gson().fromJson(jsonTxt, JsonObject.class);
if (json.get("inputs") == null || json.get("outputs") == null) {
throw new JsonSyntaxException("missing vin/vout");
}
JsonArray jsonVin = json.get("inputs").getAsJsonArray();
JsonArray jsonVout = json.get("outputs").getAsJsonArray();
if (jsonVin == null || jsonVout == null || jsonVin.size() < 1 || jsonVout.size() < 1) {
throw new JsonSyntaxException("not enough vins/vouts");
}
return new Pair<>(jsonVin, jsonVout);
}
}
93 changes: 93 additions & 0 deletions social/src/main/java/bisq/social/userprofile/BtcTxValidator.java
Original file line number Diff line number Diff line change
@@ -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.social.userprofile;

import bisq.common.data.Pair;
import bisq.common.encoding.Hex;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;

import lombok.extern.slf4j.Slf4j;

import java.security.PublicKey;

@Slf4j
public class BtcTxValidator {

private static int PUBLIC_KEY_LENGTH = 33;

public static boolean initialSanityChecks(String txId, String jsonTxt) {
if (jsonTxt == null || jsonTxt.length() == 0) {
return false;
}
JsonObject json = new Gson().fromJson(jsonTxt, JsonObject.class);
// there should always be "txid" string element at the top level
if (json.get("txid") == null) {
return false;
}
// txid should match what we requested
if (!txId.equals(json.get("txid").getAsString())) {
return false;
}
return true;
}

public static String getFirstInputPubKey(String jsonTxt) {
try {
Pair<JsonArray, JsonArray> vinAndVout = getVinAndVout(jsonTxt);
JsonArray vinArray = vinAndVout.first();
for (JsonElement x : vinArray) {
JsonObject vin = x.getAsJsonObject();
// pubKey in witness or scriptsig (legacy or segwit txs)
JsonArray witnesses = vin.getAsJsonArray("witness");
if (witnesses != null) {
String witnessPubKey = witnesses.get(1).getAsString();
if (witnessPubKey.length() >= PUBLIC_KEY_LENGTH * 2) {
return witnessPubKey;
}
}
JsonElement scriptsig = vin.get("scriptsig");
if (scriptsig != null) {
String scriptsigAsHex = scriptsig.getAsString();
if (scriptsigAsHex.length() >= PUBLIC_KEY_LENGTH * 2) {
return scriptsigAsHex.substring(scriptsigAsHex.length() - PUBLIC_KEY_LENGTH * 2);
}
}
}
} catch (JsonSyntaxException e) {
log.error("json error:", e);
}
throw new JsonSyntaxException("could not find pubKey");
}

private static Pair<JsonArray, JsonArray> getVinAndVout(String jsonTxt) throws JsonSyntaxException {
JsonObject json = new Gson().fromJson(jsonTxt, JsonObject.class);
if (json.get("vin") == null || json.get("vout") == null) {
throw new JsonSyntaxException("missing vin/vout");
}
JsonArray jsonVin = json.get("vin").getAsJsonArray();
JsonArray jsonVout = json.get("vout").getAsJsonArray();
if (jsonVin == null || jsonVout == null || jsonVin.size() < 1 || jsonVout.size() < 1) {
throw new JsonSyntaxException("not enough vins/vouts");
}
return new Pair<>(jsonVin, jsonVout);
}
}
Loading

0 comments on commit 3f4bfa3

Please sign in to comment.