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

Arbitrator witness signing #4280

Merged
merged 8 commits into from
Jul 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions common/src/main/java/bisq/common/util/Utilities.java
Original file line number Diff line number Diff line change
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ public boolean isSignedByArbitrator() {
// Getters
///////////////////////////////////////////////////////////////////////////////////////////

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

Expand Down
Original file line number Diff line number Diff line change
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 @@ -52,25 +53,28 @@
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
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 +127,8 @@ public void onUpdatedDataReceived() {
}
});
}
// TODO: Enable cleaning of signed witness list when necessary
// cleanSignedWitnesses();
}

private void onBootstrapComplete() {
Expand Down Expand Up @@ -176,7 +182,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 @@ -195,9 +208,42 @@ 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,11 +252,12 @@ 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 "";
}

public void selfSignAccountAgeWitness(AccountAgeWitness accountAgeWitness) throws CryptoException {
Expand Down Expand Up @@ -306,6 +353,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 Down Expand Up @@ -405,7 +470,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 @@ -420,4 +484,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());
// }
// }));
// }
}
Loading