Skip to content
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

Send signed witness to counterparty #4314

Closed
18 changes: 14 additions & 4 deletions common/src/main/java/bisq/common/util/Utilities.java
Expand Up @@ -42,10 +42,6 @@
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;

import javax.crypto.Cipher;

import java.security.NoSuchAlgorithmException;

import java.net.URI;
import java.net.URISyntaxException;

Expand All @@ -56,15 +52,19 @@
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -330,6 +330,10 @@ public static boolean isAltPressed(KeyCode keyCode, KeyEvent keyEvent) {
return new KeyCodeCombination(keyCode, KeyCombination.ALT_DOWN).match(keyEvent);
}

public static boolean isCtrlShiftPressed(KeyCode keyCode, KeyEvent keyEvent) {
return new KeyCodeCombination(keyCode, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN).match(keyEvent);
}

public static byte[] concatenateByteArrays(byte[] array1, byte[] array2) {
return ArrayUtils.addAll(array1, array2);
}
Expand Down Expand Up @@ -452,4 +456,10 @@ public static int byteArrayToInteger(byte[] bytes) {
}
return result;
}

// Helper to filter unique elements by key
public static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) {
Map<Object, Boolean> map = new ConcurrentHashMap<>();
return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}
}
4 changes: 2 additions & 2 deletions core/src/main/java/bisq/core/account/sign/SignedWitness.java
Expand Up @@ -117,7 +117,7 @@ public protobuf.PersistableNetworkPayload toProtoMessage() {
return protobuf.PersistableNetworkPayload.newBuilder().setSignedWitness(builder).build();
}

protobuf.SignedWitness toProtoSignedWitness() {
public protobuf.SignedWitness toProtoSignedWitness() {
return toProtoMessage().getSignedWitness();
}

Expand Down Expand Up @@ -168,7 +168,7 @@ public boolean isSignedByArbitrator() {
// Getters
///////////////////////////////////////////////////////////////////////////////////////////

P2PDataStorage.ByteArray getHashAsByteArray() {
public P2PDataStorage.ByteArray getHashAsByteArray() {
return new P2PDataStorage.ByteArray(hash);
}

Expand Down
125 changes: 112 additions & 13 deletions core/src/main/java/bisq/core/account/sign/SignedWitnessService.java
Expand Up @@ -29,6 +29,7 @@

import bisq.common.UserThread;
import bisq.common.crypto.CryptoException;
import bisq.common.crypto.Hash;
import bisq.common.crypto.KeyRing;
import bisq.common.crypto.Sig;
import bisq.common.util.Utilities;
Expand All @@ -50,27 +51,33 @@

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
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.Stack;
import java.util.stream.Collector;
import java.util.stream.Collectors;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SignedWitnessService {
public static final long SIGNER_AGE_DAYS = 30;
private static final long SIGNER_AGE = SIGNER_AGE_DAYS * ChronoUnit.DAYS.getDuration().toMillis();
static final Coin MINIMUM_TRADE_AMOUNT_FOR_SIGNING = Coin.parseCoin("0.0025");
public static final Coin MINIMUM_TRADE_AMOUNT_FOR_SIGNING = Coin.parseCoin("0.0025");

private final KeyRing keyRing;
private final P2PService p2PService;
private final ArbitratorManager arbitratorManager;
private final User user;

@Getter
private final Map<P2PDataStorage.ByteArray, SignedWitness> signedWitnessMap = new HashMap<>();
private final FilterManager filterManager;

Expand Down Expand Up @@ -123,6 +130,8 @@ public void onUpdatedDataReceived() {
}
});
}
// TODO: Enable cleaning of signed witness list when necessary
// cleanSignedWitnesses();
}

private void onBootstrapComplete() {
Expand Down Expand Up @@ -176,7 +185,14 @@ public boolean isFilteredWitness(AccountAgeWitness accountAgeWitness) {
.anyMatch(ownerPubKey -> filterManager.isSignerPubKeyBanned(Utils.HEX.encode(ownerPubKey)));
}

public String ownerPubKey(AccountAgeWitness accountAgeWitness) {
private byte[] ownerPubKey(AccountAgeWitness accountAgeWitness) {
return getSignedWitnessSet(accountAgeWitness).stream()
.map(SignedWitness::getWitnessOwnerPubKey)
.findFirst()
.orElse(null);
}

public String ownerPubKeyAsString(AccountAgeWitness accountAgeWitness) {
return getSignedWitnessSet(accountAgeWitness).stream()
.map(signedWitness -> Utils.HEX.encode(signedWitness.getWitnessOwnerPubKey()))
.findFirst()
Expand All @@ -190,14 +206,58 @@ public Set<SignedWitness> getSignedWitnessSetByOwnerPubKey(byte[] ownerPubKey) {
.collect(Collectors.toSet());
}

public boolean publishOwnSignedWitness(SignedWitness signedWitness) {
if (!Arrays.equals(signedWitness.getWitnessOwnerPubKey(), keyRing.getPubKeyRing().getSignaturePubKeyBytes()) ||
!verifySigner(signedWitness)) {
return false;
}

log.info("Publish own signedWitness {}", signedWitness);
publishSignedWitness(signedWitness);
return true;
}

// Arbitrators sign with EC key
public void signAccountAgeWitness(Coin tradeAmount,
AccountAgeWitness accountAgeWitness,
ECKey key,
PublicKey peersPubKey) {
signAccountAgeWitness(tradeAmount, accountAgeWitness, key, peersPubKey.getEncoded(), new Date().getTime());
}

// Arbitrators sign with EC key
public String signAccountAgeWitness(AccountAgeWitness accountAgeWitness,
ECKey key,
byte[] peersPubKey,
long time) {
var witnessPubKey = peersPubKey == null ? ownerPubKey(accountAgeWitness) : peersPubKey;
return signAccountAgeWitness(MINIMUM_TRADE_AMOUNT_FOR_SIGNING, accountAgeWitness, key, witnessPubKey, time);
}

// Arbitrators sign with EC key
public String signTraderPubKey(ECKey key,
byte[] peersPubKey,
long childSignTime) {
var time = childSignTime - SIGNER_AGE - 1;
var dummyAccountAgeWitness = new AccountAgeWitness(Hash.getRipemd160hash(peersPubKey), time);
return signAccountAgeWitness(MINIMUM_TRADE_AMOUNT_FOR_SIGNING, dummyAccountAgeWitness, key, peersPubKey, time);
}

// Arbitrators sign with EC key
private String signAccountAgeWitness(Coin tradeAmount,
AccountAgeWitness accountAgeWitness,
ECKey key,
byte[] peersPubKey,
long time) {
if (isSignedAccountAgeWitness(accountAgeWitness)) {
log.warn("Arbitrator trying to sign already signed accountagewitness {}", accountAgeWitness.toString());
return;
var err = "Arbitrator trying to sign already signed accountagewitness " + accountAgeWitness.toString();
log.warn(err);
return err;
}
if (peersPubKey == null) {
var err = "Trying to sign accountAgeWitness " + accountAgeWitness.toString() + "\nwith owner pubkey=null";
log.warn(err);
return err;
}

String accountAgeWitnessHashAsHex = Utilities.encodeToHex(accountAgeWitness.getHash());
Expand All @@ -206,25 +266,26 @@ public void signAccountAgeWitness(Coin tradeAmount,
accountAgeWitness.getHash(),
signatureBase64.getBytes(Charsets.UTF_8),
key.getPubKey(),
peersPubKey.getEncoded(),
new Date().getTime(),
peersPubKey,
time,
tradeAmount.value);
publishSignedWitness(signedWitness);
log.info("Arbitrator signed witness {}", signedWitness.toString());
return "";
}

// Any peer can sign with DSA key
public void signAccountAgeWitness(Coin tradeAmount,
AccountAgeWitness accountAgeWitness,
PublicKey peersPubKey) throws CryptoException {
public Optional<SignedWitness> signAccountAgeWitness(Coin tradeAmount,
AccountAgeWitness accountAgeWitness,
PublicKey peersPubKey) throws CryptoException {
if (isSignedAccountAgeWitness(accountAgeWitness)) {
log.warn("Trader trying to sign already signed accountagewitness {}", accountAgeWitness.toString());
return;
return Optional.empty();
}

if (!isSufficientTradeAmountForSigning(tradeAmount)) {
log.warn("Trader tried to sign account with too little trade amount");
return;
return Optional.empty();
}

byte[] signature = Sig.sign(keyRing.getSignatureKeyPair().getPrivate(), accountAgeWitness.getHash());
Expand All @@ -237,6 +298,7 @@ public void signAccountAgeWitness(Coin tradeAmount,
tradeAmount.value);
publishSignedWitness(signedWitness);
log.info("Trader signed witness {}", signedWitness.toString());
return Optional.of(signedWitness);
}

public boolean verifySignature(SignedWitness signedWitness) {
Expand Down Expand Up @@ -300,6 +362,24 @@ public Set<SignedWitness> getTrustedPeerSignedWitnessSet(AccountAgeWitness accou
.collect(Collectors.toSet());
}

public Set<SignedWitness> getRootSignedWitnessSet(boolean includeSignedByArbitrator) {
return signedWitnessMap.values().stream()
.filter(witness -> getSignedWitnessSetByOwnerPubKey(witness.getSignerPubKey(), new Stack<>()).isEmpty())
.filter(witness -> includeSignedByArbitrator ||
witness.getVerificationMethod() != SignedWitness.VerificationMethod.ARBITRATOR)
.collect(Collectors.toSet());
}

// Find first (in time) SignedWitness per missing signer
public Set<SignedWitness> getUnsignedSignerPubKeys() {
var oldestUnsignedSigners = new HashMap<P2PDataStorage.ByteArray, SignedWitness>();
getRootSignedWitnessSet(false).forEach(signedWitness ->
oldestUnsignedSigners.compute(new P2PDataStorage.ByteArray(signedWitness.getSignerPubKey()),
(key, oldValue) -> oldValue == null ? signedWitness :
oldValue.getDate() > signedWitness.getDate() ? signedWitness : oldValue));
return new HashSet<>(oldestUnsignedSigners.values());
}

// We go one level up by using the signer Key to lookup for SignedWitness objects which contain the signerKey as
// witnessOwnerPubKey
private Set<SignedWitness> getSignedWitnessSetByOwnerPubKey(byte[] ownerPubKey,
Expand All @@ -322,6 +402,11 @@ public boolean isSufficientTradeAmountForSigning(Coin tradeAmount) {
return !tradeAmount.isLessThan(MINIMUM_TRADE_AMOUNT_FOR_SIGNING);
}

private boolean verifySigner(SignedWitness signedWitness) {
return getSignedWitnessSetByOwnerPubKey(signedWitness.getWitnessOwnerPubKey(), new Stack<>()).stream()
.anyMatch(w -> isValidSignerWitnessInternal(w, signedWitness.getDate(), new Stack<>()));
}

/**
* Checks whether the accountAgeWitness has a valid signature from a peer/arbitrator and is allowed to sign
* other accounts.
Expand All @@ -346,7 +431,7 @@ private boolean isSignerAccountAgeWitness(AccountAgeWitness accountAgeWitness, l
* Helper to isValidAccountAgeWitness(accountAgeWitness)
*
* @param signedWitness the signedWitness to validate
* @param childSignedWitnessDateMillis the date the child SignedWitness was signed or current time if it is a leave.
* @param childSignedWitnessDateMillis the date the child SignedWitness was signed or current time if it is a leaf.
* @param excludedPubKeys stack to prevent recursive loops
* @return true if signedWitness is valid, false otherwise.
*/
Expand Down Expand Up @@ -399,7 +484,6 @@ private boolean verifyDate(SignedWitness signedWitness, long childSignedWitnessD

@VisibleForTesting
void addToMap(SignedWitness signedWitness) {
// TODO: Perhaps filter out all but one signedwitness per accountagewitness
signedWitnessMap.putIfAbsent(signedWitness.getHashAsByteArray(), signedWitness);
}

Expand All @@ -414,4 +498,19 @@ private void publishSignedWitness(SignedWitness signedWitness) {
private void doRepublishAllSignedWitnesses() {
signedWitnessMap.forEach((e, signedWitness) -> p2PService.addPersistableNetworkPayload(signedWitness, true));
}

// Remove SignedWitnesses that are signed by TRADE that also have an ARBITRATOR signature
// for the same ownerPubKey and AccountAgeWitnessHash
private void cleanSignedWitnesses() {
var orphans = getRootSignedWitnessSet(false);
var signedWitnessesCopy = new HashSet<>(signedWitnessMap.values());
signedWitnessesCopy.forEach(sw -> orphans.forEach(orphan -> {
if (sw.getVerificationMethod() == SignedWitness.VerificationMethod.ARBITRATOR &&
Arrays.equals(sw.getWitnessOwnerPubKey(), orphan.getWitnessOwnerPubKey()) &&
Arrays.equals(sw.getAccountAgeWitnessHash(), orphan.getAccountAgeWitnessHash())) {
signedWitnessMap.remove(orphan.getHashAsByteArray());
log.info("Remove duplicate SignedWitness: {}", orphan.toString());
}
}));
}
}