Merge pull request #4280 from sqrrm/arbitrator-witness-signing

Arbitrator witness signing
This commit is contained in:
Christoph Atteneder 2020-07-03 17:57:17 +02:00 committed by GitHub
commit c190f4200f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 910 additions and 102 deletions

View File

@ -42,10 +42,6 @@ import javafx.scene.input.KeyCodeCombination;
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;
@ -56,15 +52,19 @@ import java.util.Date;
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;
@ -330,6 +330,10 @@ public class Utilities {
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);
}
@ -452,4 +456,10 @@ public class Utilities {
}
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;
}
}

View File

@ -168,7 +168,7 @@ public class SignedWitness implements ProcessOncePersistableNetworkPayload, Pers
// Getters
///////////////////////////////////////////////////////////////////////////////////////////
P2PDataStorage.ByteArray getHashAsByteArray() {
public P2PDataStorage.ByteArray getHashAsByteArray() {
return new P2PDataStorage.ByteArray(hash);
}

View File

@ -29,6 +29,7 @@ import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreService;
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;
@ -52,25 +53,28 @@ import java.util.ArrayList;
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;
@ -123,6 +127,8 @@ public class SignedWitnessService {
}
});
}
// TODO: Enable cleaning of signed witness list when necessary
// cleanSignedWitnesses();
}
private void onBootstrapComplete() {
@ -176,7 +182,14 @@ public class SignedWitnessService {
.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()
@ -195,9 +208,42 @@ public class SignedWitnessService {
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());
@ -206,11 +252,12 @@ public class SignedWitnessService {
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 {
@ -306,6 +353,24 @@ public class SignedWitnessService {
.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,
@ -405,7 +470,6 @@ public class SignedWitnessService {
@VisibleForTesting
void addToMap(SignedWitness signedWitness) {
// TODO: Perhaps filter out all but one signedwitness per accountagewitness
signedWitnessMap.putIfAbsent(signedWitness.getHashAsByteArray(), signedWitness);
}
@ -420,4 +484,19 @@ public class SignedWitnessService {
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());
// }
// }));
// }
}

View File

@ -17,6 +17,7 @@
package bisq.core.account.witness;
import bisq.core.account.sign.SignedWitness;
import bisq.core.account.sign.SignedWitnessService;
import bisq.core.filter.FilterManager;
import bisq.core.filter.PaymentAccountFilter;
@ -33,7 +34,6 @@ import bisq.core.payment.payload.PaymentMethod;
import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeResult;
import bisq.core.support.dispute.arbitration.TraderDataItem;
import bisq.core.trade.Contract;
import bisq.core.trade.Trade;
import bisq.core.trade.protocol.TradingPeer;
import bisq.core.user.User;
@ -72,14 +72,14 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
@ -130,7 +130,10 @@ public class AccountAgeWitnessService {
private final SignedWitnessService signedWitnessService;
private final ChargeBackRisk chargeBackRisk;
private final FilterManager filterManager;
@Getter
private final AccountAgeWitnessUtils accountAgeWitnessUtils;
@Getter
private final Map<P2PDataStorage.ByteArray, AccountAgeWitness> accountAgeWitnessMap = new HashMap<>();
@ -155,6 +158,11 @@ public class AccountAgeWitnessService {
this.chargeBackRisk = chargeBackRisk;
this.filterManager = filterManager;
accountAgeWitnessUtils = new AccountAgeWitnessUtils(
this,
signedWitnessService,
keyRing);
// We need to add that early (before onAllServicesInitialized) as it will be used at startup.
appendOnlyDataStoreService.addService(accountAgeWitnessStorageService);
}
@ -225,11 +233,11 @@ public class AccountAgeWitnessService {
public byte[] getPeerAccountAgeWitnessHash(Trade trade) {
return findTradePeerWitness(trade)
.map(accountAgeWitness -> accountAgeWitness.getHash())
.map(AccountAgeWitness::getHash)
.orElse(null);
}
private byte[] getAccountInputDataWithSalt(PaymentAccountPayload paymentAccountPayload) {
byte[] getAccountInputDataWithSalt(PaymentAccountPayload paymentAccountPayload) {
return Utilities.concatenateByteArrays(paymentAccountPayload.getAgeWitnessInputData(), paymentAccountPayload.getSalt());
}
@ -241,8 +249,8 @@ public class AccountAgeWitnessService {
return new AccountAgeWitness(hash, new Date().getTime());
}
private Optional<AccountAgeWitness> findWitness(PaymentAccountPayload paymentAccountPayload,
PubKeyRing pubKeyRing) {
Optional<AccountAgeWitness> findWitness(PaymentAccountPayload paymentAccountPayload,
PubKeyRing pubKeyRing) {
byte[] accountInputDataWithSalt = getAccountInputDataWithSalt(paymentAccountPayload);
byte[] hash = Hash.getSha256Ripemd160hash(Utilities.concatenateByteArrays(accountInputDataWithSalt,
pubKeyRing.getSignaturePubKeyBytes()));
@ -634,6 +642,32 @@ public class AccountAgeWitnessService {
signedWitnessService.signAccountAgeWitness(tradeAmount, accountAgeWitness, key, peersPubKey);
}
public String arbitratorSignOrphanWitness(AccountAgeWitness accountAgeWitness,
ECKey key,
long time) {
// Find AccountAgeWitness as signedwitness
var signedWitness = signedWitnessService.getSignedWitnessMap().values().stream()
.filter(sw -> Arrays.equals(sw.getAccountAgeWitnessHash(), accountAgeWitness.getHash()))
.findAny()
.orElse(null);
checkNotNull(signedWitness);
return signedWitnessService.signAccountAgeWitness(accountAgeWitness, key, signedWitness.getWitnessOwnerPubKey(),
time);
}
public String arbitratorSignOrphanPubKey(ECKey key,
byte[] peersPubKey,
long childSignTime) {
return signedWitnessService.signTraderPubKey(key, peersPubKey, childSignTime);
}
public void arbitratorSignAccountAgeWitness(AccountAgeWitness accountAgeWitness,
ECKey key,
byte[] tradersPubKey,
long time) {
signedWitnessService.signAccountAgeWitness(accountAgeWitness, key, tradersPubKey, time);
}
public void traderSignPeersAccountAgeWitness(Trade trade) {
AccountAgeWitness peersWitness = findTradePeerWitness(trade).orElse(null);
Coin tradeAmount = trade.getTradeAmount();
@ -758,7 +792,7 @@ public class AccountAgeWitnessService {
public SignState getSignState(AccountAgeWitness accountAgeWitness) {
// Add hash to sign state info when running in debug mode
String hash = log.isDebugEnabled() ? Utilities.bytesAsHexString(accountAgeWitness.getHash()) + "\n" +
signedWitnessService.ownerPubKey(accountAgeWitness) : "";
signedWitnessService.ownerPubKeyAsString(accountAgeWitness) : "";
if (signedWitnessService.isFilteredWitness(accountAgeWitness)) {
return SignState.BANNED.addHash(hash);
}
@ -779,6 +813,13 @@ public class AccountAgeWitnessService {
}
}
public Set<AccountAgeWitness> getOrphanSignedWitnesses() {
return signedWitnessService.getRootSignedWitnessSet(false).stream()
.map(signedWitness -> getWitnessByHash(signedWitness.getAccountAgeWitnessHash()).orElse(null))
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}
public void signSameNameAccounts() {
// Collect accounts that have ownerId to sign unsigned accounts with the same ownderId
var signerAccounts = Objects.requireNonNull(user.getPaymentAccounts()).stream()
@ -803,63 +844,7 @@ public class AccountAgeWitnessService {
}));
}
///////////////////////////////////////////////////////////////////////////////////////////
// Debug logs
///////////////////////////////////////////////////////////////////////////////////////////
private String getWitnessDebugLog(PaymentAccountPayload paymentAccountPayload,
PubKeyRing pubKeyRing) {
Optional<AccountAgeWitness> accountAgeWitness = findWitness(paymentAccountPayload, pubKeyRing);
if (!accountAgeWitness.isPresent()) {
byte[] accountInputDataWithSalt = getAccountInputDataWithSalt(paymentAccountPayload);
byte[] hash = Hash.getSha256Ripemd160hash(Utilities.concatenateByteArrays(accountInputDataWithSalt,
pubKeyRing.getSignaturePubKeyBytes()));
return "No accountAgeWitness found for paymentAccountPayload with hash " + Utilities.bytesAsHexString(hash);
}
SignState signState = getSignState(accountAgeWitness.get());
return signState.name() + " " + signState.getPresentation() +
"\n" + accountAgeWitness.toString();
}
public void witnessDebugLog(Trade trade, @Nullable AccountAgeWitness myWitness) {
// Log to find why accounts sometimes don't get signed as expected
// TODO: Demote to debug or remove once account signing is working ok
checkNotNull(trade.getContract());
checkNotNull(trade.getContract().getBuyerPaymentAccountPayload());
boolean checkingSignTrade = true;
boolean isBuyer = trade.getContract().isMyRoleBuyer(keyRing.getPubKeyRing());
AccountAgeWitness witness = myWitness;
if (witness == null) {
witness = isBuyer ?
getMyWitness(trade.getContract().getBuyerPaymentAccountPayload()) :
getMyWitness(trade.getContract().getSellerPaymentAccountPayload());
checkingSignTrade = false;
}
boolean isSignWitnessTrade = accountIsSigner(witness) &&
!peerHasSignedWitness(trade) &&
tradeAmountIsSufficient(trade.getTradeAmount());
log.info("AccountSigning: " +
"\ntradeId: {}" +
"\nis buyer: {}" +
"\nbuyer account age witness info: {}" +
"\nseller account age witness info: {}" +
"\nchecking for sign trade: {}" +
"\nis myWitness signer: {}" +
"\npeer has signed witness: {}" +
"\ntrade amount: {}" +
"\ntrade amount is sufficient: {}" +
"\nisSignWitnessTrade: {}",
trade.getId(),
isBuyer,
getWitnessDebugLog(trade.getContract().getBuyerPaymentAccountPayload(),
trade.getContract().getBuyerPubKeyRing()),
getWitnessDebugLog(trade.getContract().getSellerPaymentAccountPayload(),
trade.getContract().getSellerPubKeyRing()),
checkingSignTrade, // Following cases added to use same logic as in seller signing check
accountIsSigner(witness),
peerHasSignedWitness(trade),
trade.getTradeAmount(),
tradeAmountIsSufficient(trade.getTradeAmount()),
isSignWitnessTrade);
public Set<SignedWitness> getUnsignedSignerPubKeys() {
return signedWitnessService.getUnsignedSignerPubKeys();
}
}

View File

@ -0,0 +1,171 @@
/*
* 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.account.witness;
import bisq.core.account.sign.SignedWitness;
import bisq.core.account.sign.SignedWitnessService;
import bisq.core.payment.payload.PaymentAccountPayload;
import bisq.core.trade.Trade;
import bisq.network.p2p.storage.P2PDataStorage;
import bisq.common.crypto.Hash;
import bisq.common.crypto.KeyRing;
import bisq.common.crypto.PubKeyRing;
import bisq.common.util.Utilities;
import java.util.Arrays;
import java.util.Optional;
import java.util.Stack;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
public class AccountAgeWitnessUtils {
private final AccountAgeWitnessService accountAgeWitnessService;
private final SignedWitnessService signedWitnessService;
private final KeyRing keyRing;
AccountAgeWitnessUtils(AccountAgeWitnessService accountAgeWitnessService,
SignedWitnessService signedWitnessService,
KeyRing keyRing) {
this.accountAgeWitnessService = accountAgeWitnessService;
this.signedWitnessService = signedWitnessService;
this.keyRing = keyRing;
}
// Log tree of signed witnesses
public void logSignedWitnesses() {
var orphanSigners = signedWitnessService.getRootSignedWitnessSet(true);
log.info("Orphaned signed account age witnesses:");
orphanSigners.forEach(w -> {
log.info("{}: Signer PKH: {} Owner PKH: {} time: {}", w.getVerificationMethod().toString(),
Utilities.bytesAsHexString(Hash.getRipemd160hash(w.getSignerPubKey())).substring(0, 7),
Utilities.bytesAsHexString(Hash.getRipemd160hash(w.getWitnessOwnerPubKey())).substring(0, 7),
w.getDate());
logChild(w, " ", new Stack<>());
});
}
private void logChild(SignedWitness sigWit, String initString, Stack<P2PDataStorage.ByteArray> excluded) {
var allSig = signedWitnessService.getSignedWitnessMap();
log.info("{}AEW: {} PKH: {} time: {}", initString,
Utilities.bytesAsHexString(sigWit.getAccountAgeWitnessHash()).substring(0, 7),
Utilities.bytesAsHexString(Hash.getRipemd160hash(sigWit.getWitnessOwnerPubKey())).substring(0, 7),
sigWit.getDate());
allSig.values().forEach(w -> {
if (!excluded.contains(new P2PDataStorage.ByteArray(w.getWitnessOwnerPubKey())) &&
Arrays.equals(w.getSignerPubKey(), sigWit.getWitnessOwnerPubKey())) {
excluded.push(new P2PDataStorage.ByteArray(w.getWitnessOwnerPubKey()));
logChild(w, initString + " ", excluded);
excluded.pop();
}
});
}
// Log signers per
public void logSigners() {
log.info("Signers per AEW");
var allSig = signedWitnessService.getSignedWitnessMap();
allSig.values().forEach(w -> {
log.info("AEW {}", Utilities.bytesAsHexString(w.getAccountAgeWitnessHash()));
allSig.values().forEach(ww -> {
if (Arrays.equals(w.getSignerPubKey(), ww.getWitnessOwnerPubKey())) {
log.info(" {}", Utilities.bytesAsHexString(ww.getAccountAgeWitnessHash()));
}
});
}
);
}
public void logUnsignedSignerPubKeys() {
log.info("Unsigned signer pubkeys");
signedWitnessService.getUnsignedSignerPubKeys().forEach(signedWitness ->
log.info("PK hash {} date {}",
Utilities.bytesAsHexString(Hash.getRipemd160hash(signedWitness.getSignerPubKey())),
signedWitness.getDate()));
}
///////////////////////////////////////////////////////////////////////////////////////////
// Debug logs
///////////////////////////////////////////////////////////////////////////////////////////
private String getWitnessDebugLog(PaymentAccountPayload paymentAccountPayload,
PubKeyRing pubKeyRing) {
Optional<AccountAgeWitness> accountAgeWitness =
accountAgeWitnessService.findWitness(paymentAccountPayload, pubKeyRing);
if (!accountAgeWitness.isPresent()) {
byte[] accountInputDataWithSalt =
accountAgeWitnessService.getAccountInputDataWithSalt(paymentAccountPayload);
byte[] hash = Hash.getSha256Ripemd160hash(Utilities.concatenateByteArrays(accountInputDataWithSalt,
pubKeyRing.getSignaturePubKeyBytes()));
return "No accountAgeWitness found for paymentAccountPayload with hash " + Utilities.bytesAsHexString(hash);
}
AccountAgeWitnessService.SignState signState =
accountAgeWitnessService.getSignState(accountAgeWitness.get());
return signState.name() + " " + signState.getPresentation() +
"\n" + accountAgeWitness.toString();
}
public void witnessDebugLog(Trade trade, @Nullable AccountAgeWitness myWitness) {
// Log to find why accounts sometimes don't get signed as expected
// TODO: Demote to debug or remove once account signing is working ok
checkNotNull(trade.getContract());
checkNotNull(trade.getContract().getBuyerPaymentAccountPayload());
boolean checkingSignTrade = true;
boolean isBuyer = trade.getContract().isMyRoleBuyer(keyRing.getPubKeyRing());
AccountAgeWitness witness = myWitness;
if (witness == null) {
witness = isBuyer ?
accountAgeWitnessService.getMyWitness(trade.getContract().getBuyerPaymentAccountPayload()) :
accountAgeWitnessService.getMyWitness(trade.getContract().getSellerPaymentAccountPayload());
checkingSignTrade = false;
}
boolean isSignWitnessTrade = accountAgeWitnessService.accountIsSigner(witness) &&
!accountAgeWitnessService.peerHasSignedWitness(trade) &&
accountAgeWitnessService.tradeAmountIsSufficient(trade.getTradeAmount());
log.info("AccountSigning: " +
"\ntradeId: {}" +
"\nis buyer: {}" +
"\nbuyer account age witness info: {}" +
"\nseller account age witness info: {}" +
"\nchecking for sign trade: {}" +
"\nis myWitness signer: {}" +
"\npeer has signed witness: {}" +
"\ntrade amount: {}" +
"\ntrade amount is sufficient: {}" +
"\nisSignWitnessTrade: {}",
trade.getId(),
isBuyer,
getWitnessDebugLog(trade.getContract().getBuyerPaymentAccountPayload(),
trade.getContract().getBuyerPubKeyRing()),
getWitnessDebugLog(trade.getContract().getSellerPaymentAccountPayload(),
trade.getContract().getSellerPubKeyRing()),
checkingSignTrade, // Following cases added to use same logic as in seller signing check
accountAgeWitnessService.accountIsSigner(witness),
accountAgeWitnessService.peerHasSignedWitness(trade),
trade.getTradeAmount(),
accountAgeWitnessService.tradeAmountIsSufficient(trade.getTradeAmount()),
isSignWitnessTrade);
}
}

View File

@ -358,6 +358,6 @@ public class ProcessModel implements Model, PersistablePayload {
}
void logTrade(Trade trade) {
accountAgeWitnessService.witnessDebugLog(trade, null);
accountAgeWitnessService.getAccountAgeWitnessUtils().witnessDebugLog(trade, null);
}
}

View File

@ -2730,6 +2730,23 @@ popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts
popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts \
and the initial limit for one of your accounts has been lifted.\n\n{0}
popup.accountSigning.singleAccountSelect.headline=Select account age witness
popup.accountSigning.singleAccountSelect.description=Search for account age witness.
popup.accountSigning.singleAccountSelect.datePicker=Select point of time for signing
popup.accountSigning.confirmSingleAccount.headline=Confirm selected account age witness
popup.accountSigning.confirmSingleAccount.selectedHash=Selected witness hash
popup.accountSigning.confirmSingleAccount.button=Sign account age witness
popup.accountSigning.successSingleAccount.description=Witness {0} was signed
popup.accountSigning.successSingleAccount.success.headline=Success
popup.accountSigning.successSingleAccount.signError=Failed to sign witness, {0}
popup.accountSigning.unsignedPubKeys.headline=Unsigned Pubkeys
popup.accountSigning.unsignedPubKeys.sign=Sign Pubkeys
popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed
popup.accountSigning.unsignedPubKeys.result.headline=Signing completed
popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys
popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign
####################################################################
# Notifications
####################################################################

View File

@ -37,6 +37,7 @@ import bisq.network.p2p.P2PService;
import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreService;
import bisq.common.crypto.CryptoException;
import bisq.common.crypto.Hash;
import bisq.common.crypto.KeyRing;
import bisq.common.crypto.KeyStorage;
import bisq.common.crypto.PubKeyRing;
@ -53,9 +54,11 @@ import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Before;
@ -81,12 +84,25 @@ public class AccountAgeWitnessServiceTest {
private AccountAgeWitnessService service;
private ChargeBackRisk chargeBackRisk;
private FilterManager filterManager;
private File dir1;
private File dir2;
private File dir3;
@Before
public void setup() {
public void setup() throws IOException {
KeyRing keyRing = mock(KeyRing.class);
setupService(keyRing);
keypair = Sig.generateKeyPair();
publicKey = keypair.getPublic();
// Setup temp storage dir
dir1 = makeDir("temp_tests1");
dir2 = makeDir("temp_tests1");
dir3 = makeDir("temp_tests1");
}
private void setupService(KeyRing keyRing) {
chargeBackRisk = mock(ChargeBackRisk.class);
AppendOnlyDataStoreService dataStoreService = mock(AppendOnlyDataStoreService.class);
KeyRing keyRing = mock(KeyRing.class);
P2PService p2pService = mock(P2PService.class);
ArbitratorManager arbitratorManager = mock(ArbitratorManager.class);
when(arbitratorManager.isPublicKeyInList(any())).thenReturn(true);
@ -94,8 +110,13 @@ public class AccountAgeWitnessServiceTest {
filterManager = mock(FilterManager.class);
signedWitnessService = new SignedWitnessService(keyRing, p2pService, arbitratorManager, null, appendOnlyDataStoreService, null, filterManager);
service = new AccountAgeWitnessService(null, null, null, signedWitnessService, chargeBackRisk, null, dataStoreService, filterManager);
keypair = Sig.generateKeyPair();
publicKey = keypair.getPublic();
}
private File makeDir(String name) throws IOException {
var dir = File.createTempFile(name, "");
dir.delete();
dir.mkdir();
return dir;
}
@After
@ -105,21 +126,21 @@ public class AccountAgeWitnessServiceTest {
@Ignore
@Test
public void testIsTradeDateAfterReleaseDate() throws CryptoException {
Date ageWitnessReleaseDate = new GregorianCalendar(2017, 9, 23).getTime();
Date tradeDate = new GregorianCalendar(2017, 10, 1).getTime();
public void testIsTradeDateAfterReleaseDate() {
Date ageWitnessReleaseDate = new GregorianCalendar(2017, Calendar.OCTOBER, 23).getTime();
Date tradeDate = new GregorianCalendar(2017, Calendar.NOVEMBER, 1).getTime();
assertTrue(service.isDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate, errorMessage -> {
}));
tradeDate = new GregorianCalendar(2017, 9, 23).getTime();
tradeDate = new GregorianCalendar(2017, Calendar.OCTOBER, 23).getTime();
assertTrue(service.isDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate, errorMessage -> {
}));
tradeDate = new GregorianCalendar(2017, 9, 22, 0, 0, 1).getTime();
tradeDate = new GregorianCalendar(2017, Calendar.OCTOBER, 22, 0, 0, 1).getTime();
assertTrue(service.isDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate, errorMessage -> {
}));
tradeDate = new GregorianCalendar(2017, 9, 22).getTime();
tradeDate = new GregorianCalendar(2017, Calendar.OCTOBER, 22).getTime();
assertFalse(service.isDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate, errorMessage -> {
}));
tradeDate = new GregorianCalendar(2017, 9, 21).getTime();
tradeDate = new GregorianCalendar(2017, Calendar.OCTOBER, 21).getTime();
assertFalse(service.isDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate, errorMessage -> {
}));
}
@ -140,15 +161,7 @@ public class AccountAgeWitnessServiceTest {
}
@Test
public void testArbitratorSignWitness() throws IOException {
// Setup temp storage dir
File dir1 = File.createTempFile("temp_tests1", "");
dir1.delete();
dir1.mkdir();
File dir2 = File.createTempFile("temp_tests1", "");
dir2.delete();
dir2.mkdir();
public void testArbitratorSignWitness() {
KeyRing buyerKeyRing = new KeyRing(new KeyStorage(dir1));
KeyRing sellerKeyRing = new KeyRing(new KeyStorage(dir2));
@ -238,13 +251,110 @@ public class AccountAgeWitnessServiceTest {
buyerPubKeyRing.getSignaturePubKeyBytes()).stream()
.findFirst()
.orElse(null);
assert foundBuyerSignedWitness != null;
assertEquals(Utilities.bytesAsHexString(foundBuyerSignedWitness.getAccountAgeWitnessHash()),
Utilities.bytesAsHexString(buyerAccountAgeWitness.getHash()));
SignedWitness foundSellerSignedWitness = signedWitnessService.getSignedWitnessSetByOwnerPubKey(
sellerPubKeyRing.getSignaturePubKeyBytes()).stream()
.findFirst()
.orElse(null);
assert foundSellerSignedWitness != null;
assertEquals(Utilities.bytesAsHexString(foundSellerSignedWitness.getAccountAgeWitnessHash()),
Utilities.bytesAsHexString(sellerAccountAgeWitness.getHash()));
}
// Create a tree of signed witnesses Arb -(SWA)-> aew1 -(SW1)-> aew2 -(SW2)-> aew3
// Delete SWA signature, none of the account age witnesses are considered signed
// Sign a dummy AccountAgeWitness using the signerPubkey from SW1; aew2 and aew3 are not considered signed. The
// lost SignedWitness isn't possible to recover so aew1 is still not signed, but it's pubkey is a signer.
@Test
public void testArbitratorSignDummyWitness() throws CryptoException {
ECKey arbitratorKey = new ECKey();
// Init 2 user accounts
var user1KeyRing = new KeyRing(new KeyStorage(dir1));
var user2KeyRing = new KeyRing(new KeyStorage(dir2));
var user3KeyRing = new KeyRing(new KeyStorage(dir3));
var pubKeyRing1 = user1KeyRing.getPubKeyRing();
var pubKeyRing2 = user2KeyRing.getPubKeyRing();
var pubKeyRing3 = user3KeyRing.getPubKeyRing();
var account1 = new SepaAccountPayload(PaymentMethod.SEPA_ID, "1", CountryUtil.getAllSepaCountries());
var account2 = new SepaAccountPayload(PaymentMethod.SEPA_ID, "2", CountryUtil.getAllSepaCountries());
var account3 = new SepaAccountPayload(PaymentMethod.SEPA_ID, "3", CountryUtil.getAllSepaCountries());
var aew1 = service.getNewWitness(account1, pubKeyRing1);
var aew2 = service.getNewWitness(account2, pubKeyRing2);
var aew3 = service.getNewWitness(account3, pubKeyRing3);
// Backdate witness1 70 days
aew1 = new AccountAgeWitness(aew1.getHash(), new Date().getTime() - TimeUnit.DAYS.toMillis(70));
aew2 = new AccountAgeWitness(aew2.getHash(), new Date().getTime() - TimeUnit.DAYS.toMillis(35));
aew3 = new AccountAgeWitness(aew3.getHash(), new Date().getTime() - TimeUnit.DAYS.toMillis(1));
service.addToMap(aew1);
service.addToMap(aew2);
service.addToMap(aew3);
// Test as user1. It's still possible to sign as arbitrator since the ECKey is passed as an argument.
setupService(user1KeyRing);
// Arbitrator signs user1
service.arbitratorSignAccountAgeWitness(aew1, arbitratorKey, pubKeyRing1.getSignaturePubKeyBytes(),
aew1.getDate());
// user1 signs user2
signAccountAgeWitness(aew2, pubKeyRing2.getSignaturePubKey(), aew2.getDate(), user1KeyRing);
// user2 signs user3
signAccountAgeWitness(aew3, pubKeyRing3.getSignaturePubKey(), aew3.getDate(), user2KeyRing);
signedWitnessService.signAccountAgeWitness(SignedWitnessService.MINIMUM_TRADE_AMOUNT_FOR_SIGNING, aew2,
pubKeyRing2.getSignaturePubKey());
assertTrue(service.accountIsSigner(aew1));
assertTrue(service.accountIsSigner(aew2));
assertFalse(service.accountIsSigner(aew3));
assertTrue(signedWitnessService.isSignedAccountAgeWitness(aew3));
// Remove SignedWitness signed by arbitrator
@SuppressWarnings("OptionalGetWithoutIsPresent")
var signedWitnessArb = signedWitnessService.getSignedWitnessMap().values().stream()
.filter(sw -> sw.getVerificationMethod() == SignedWitness.VerificationMethod.ARBITRATOR)
.findAny()
.get();
signedWitnessService.getSignedWitnessMap().remove(signedWitnessArb.getHashAsByteArray());
assertEquals(signedWitnessService.getSignedWitnessMap().size(), 2);
// Check that no account age witness is a signer
assertFalse(service.accountIsSigner(aew1));
assertFalse(service.accountIsSigner(aew2));
assertFalse(service.accountIsSigner(aew3));
assertFalse(signedWitnessService.isSignedAccountAgeWitness(aew2));
// Sign dummy AccountAgeWitness using signer key from SW_1
assertEquals(signedWitnessService.getRootSignedWitnessSet(false).size(), 1);
// TODO: move this to accountagewitnessservice
@SuppressWarnings("OptionalGetWithoutIsPresent")
var orphanedSignedWitness = signedWitnessService.getRootSignedWitnessSet(false).stream().findAny().get();
var dummyAccountAgeWitnessHash = Hash.getRipemd160hash(orphanedSignedWitness.getSignerPubKey());
var dummyAEW = new AccountAgeWitness(dummyAccountAgeWitnessHash,
orphanedSignedWitness.getDate() -
(TimeUnit.DAYS.toMillis(SignedWitnessService.SIGNER_AGE_DAYS + 1)));
service.arbitratorSignAccountAgeWitness(
dummyAEW, arbitratorKey, orphanedSignedWitness.getSignerPubKey(), dummyAEW.getDate());
assertFalse(service.accountIsSigner(aew1));
assertTrue(service.accountIsSigner(aew2));
assertFalse(service.accountIsSigner(aew3));
assertTrue(signedWitnessService.isSignedAccountAgeWitness(aew2));
}
private void signAccountAgeWitness(AccountAgeWitness accountAgeWitness,
PublicKey witnessOwnerPubKey,
long time,
KeyRing signerKeyRing) throws CryptoException {
byte[] signature = Sig.sign(signerKeyRing.getSignatureKeyPair().getPrivate(), accountAgeWitness.getHash());
SignedWitness signedWitness = new SignedWitness(SignedWitness.VerificationMethod.TRADE,
accountAgeWitness.getHash(),
signature,
signerKeyRing.getSignatureKeyPair().getPublic().getEncoded(),
witnessOwnerPubKey.getEncoded(),
time,
SignedWitnessService.MINIMUM_TRADE_AMOUNT_FOR_SIGNING.value);
signedWitnessService.getSignedWitnessMap().putIfAbsent(signedWitness.getHashAsByteArray(), signedWitness);
}
}

View File

@ -16,6 +16,8 @@ import bisq.core.payment.PaymentAccount;
import bisq.core.payment.payload.PaymentMethod;
import bisq.common.UserThread;
import bisq.common.app.DevEnv;
import bisq.common.util.Utilities;
import org.apache.commons.lang3.StringUtils;
@ -26,10 +28,14 @@ import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.beans.value.ChangeListener;
import javafx.event.EventHandler;
import javafx.collections.ObservableList;
import javafx.util.Callback;
@ -41,8 +47,8 @@ public abstract class PaymentAccountsView<R extends Node, M extends ActivatableW
protected ListView<PaymentAccount> paymentAccountsListView;
private ChangeListener<PaymentAccount> paymentAccountChangeListener;
protected Button addAccountButton, exportButton, importButton;
SignedWitnessService signedWitnessService;
protected AccountAgeWitnessService accountAgeWitnessService;
private EventHandler<KeyEvent> keyEventEventHandler;
public PaymentAccountsView(M model, AccountAgeWitnessService accountAgeWitnessService) {
super(model);
@ -51,6 +57,16 @@ public abstract class PaymentAccountsView<R extends Node, M extends ActivatableW
@Override
public void initialize() {
keyEventEventHandler = event -> {
if (Utilities.isCtrlShiftPressed(KeyCode.L, event)) {
accountAgeWitnessService.getAccountAgeWitnessUtils().logSignedWitnesses();
} else if (Utilities.isCtrlShiftPressed(KeyCode.S, event)) {
accountAgeWitnessService.getAccountAgeWitnessUtils().logSigners();
} else if (Utilities.isCtrlShiftPressed(KeyCode.U, event)) {
accountAgeWitnessService.getAccountAgeWitnessUtils().logUnsignedSignerPubKeys();
}
};
buildForm();
paymentAccountChangeListener = (observable, oldValue, newValue) -> {
if (newValue != null)
@ -68,6 +84,8 @@ public abstract class PaymentAccountsView<R extends Node, M extends ActivatableW
addAccountButton.setOnAction(event -> addNewAccount());
exportButton.setOnAction(event -> exportAccounts());
importButton.setOnAction(event -> importAccounts());
if (root.getScene() != null)
root.getScene().addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler);
}
@Override
@ -76,6 +94,8 @@ public abstract class PaymentAccountsView<R extends Node, M extends ActivatableW
addAccountButton.setOnAction(null);
exportButton.setOnAction(null);
importButton.setOnAction(null);
if (root.getScene() != null)
root.getScene().removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler);
}
protected void onDeleteAccount(PaymentAccount paymentAccount) {

View File

@ -0,0 +1,208 @@
/*
* 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.desktop.main.overlays.windows;
import bisq.desktop.components.AutoTooltipButton;
import bisq.desktop.components.InputTextField;
import bisq.desktop.main.overlays.Overlay;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.core.account.witness.AccountAgeWitness;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.locale.Res;
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import bisq.common.util.Utilities;
import org.bitcoinj.core.Utils;
import javax.inject.Inject;
import com.jfoenix.controls.JFXAutoCompletePopup;
import javafx.scene.control.DatePicker;
import javafx.scene.control.ListCell;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.geometry.VPos;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import lombok.extern.slf4j.Slf4j;
import static bisq.desktop.util.FormBuilder.*;
@Slf4j
public class SignSpecificWitnessWindow extends Overlay<SignSpecificWitnessWindow> {
private InputTextField searchTextField;
private JFXAutoCompletePopup<AccountAgeWitness> searchAutoComplete;
private AccountAgeWitness selectedWitness;
private DatePicker datePicker;
private InputTextField privateKey;
private final AccountAgeWitnessService accountAgeWitnessService;
private final ArbitratorManager arbitratorManager;
@Inject
public SignSpecificWitnessWindow(AccountAgeWitnessService accountAgeWitnessService,
ArbitratorManager arbitratorManager) {
this.accountAgeWitnessService = accountAgeWitnessService;
this.arbitratorManager = arbitratorManager;
}
@Override
public void show() {
width = 1000;
rowIndex = -1;
createGridPane();
gridPane.setPrefHeight(600);
gridPane.getColumnConstraints().get(1).setHgrow(Priority.NEVER);
headLine(Res.get("popup.accountSigning.singleAccountSelect.headline"));
type = Type.Attention;
addHeadLine();
addSelectWitnessContent();
addButtons();
applyStyles();
display();
}
private void addSelectWitnessContent() {
searchTextField = addInputTextField(gridPane, ++rowIndex,
Res.get("popup.accountSigning.singleAccountSelect.description"));
searchAutoComplete = new JFXAutoCompletePopup<>();
searchAutoComplete.setPrefWidth(400);
searchAutoComplete.getSuggestions().addAll(accountAgeWitnessService.getOrphanSignedWitnesses());
searchAutoComplete.setSuggestionsCellFactory(param -> new ListCell<>() {
@Override
protected void updateItem(AccountAgeWitness item, boolean empty) {
super.updateItem(item, empty);
if (item != null) {
setText(Utilities.bytesAsHexString(item.getHash()));
} else {
setText("");
}
}
});
searchAutoComplete.setSelectionHandler(event -> {
searchTextField.setText(Utilities.bytesAsHexString(event.getObject().getHash()));
selectedWitness = event.getObject();
if (selectedWitness != null) {
datePicker.setValue(Instant.ofEpochMilli(selectedWitness.getDate()).atZone(
ZoneId.systemDefault()).toLocalDate());
}
});
searchTextField.textProperty().addListener(observable -> {
searchAutoComplete.filter(witness -> Utilities.bytesAsHexString(witness.getHash()).startsWith(
searchTextField.getText().toLowerCase()));
if (searchAutoComplete.getFilteredSuggestions().isEmpty()) {
searchAutoComplete.hide();
} else {
searchAutoComplete.show(searchTextField);
}
});
datePicker = addTopLabelDatePicker(gridPane, ++rowIndex,
Res.get("popup.accountSigning.singleAccountSelect.datePicker"),
0).second;
datePicker.setOnAction(e -> updateWitnessSelectionState());
}
private void addECKeyField() {
privateKey = addInputTextField(gridPane, ++rowIndex, Res.get("popup.accountSigning.signAccounts.ECKey"));
GridPane.setVgrow(privateKey, Priority.ALWAYS);
GridPane.setValignment(privateKey, VPos.TOP);
}
private void updateWitnessSelectionState() {
actionButton.setDisable(selectedWitness == null || datePicker.getValue() == null);
}
private void removeContent() {
removeRowsFromGridPane(gridPane, 1, 3);
rowIndex = 1;
}
private void selectAccountAgeWitness() {
removeContent();
headLineLabel.setText(Res.get("popup.accountSigning.confirmSingleAccount.headline"));
var selectedWitnessTextField = addTopLabelTextField(gridPane, ++rowIndex,
Res.get("popup.accountSigning.confirmSingleAccount.selectedHash")).second;
selectedWitnessTextField.setText(Utilities.bytesAsHexString(selectedWitness.getHash()));
addECKeyField();
((AutoTooltipButton) actionButton).updateText(Res.get("popup.accountSigning.confirmSingleAccount.button"));
actionButton.setOnAction(a -> {
var arbitratorKey = arbitratorManager.getRegistrationKey(privateKey.getText());
if (arbitratorKey != null) {
var arbitratorPubKeyAsHex = Utils.HEX.encode(arbitratorKey.getPubKey());
var isKeyValid = arbitratorManager.isPublicKeyInList(arbitratorPubKeyAsHex);
if (isKeyValid) {
var result = accountAgeWitnessService.arbitratorSignOrphanWitness(selectedWitness,
arbitratorKey,
datePicker.getValue().atStartOfDay().toEpochSecond(ZoneOffset.UTC) * 1000);
if (result.isEmpty()) {
addSuccessContent();
} else {
new Popup().error(Res.get("popup.accountSigning.successSingleAccount.signError", result))
.onClose(this::hide).show();
}
}
} else {
new Popup().error(Res.get("popup.accountSigning.signAccounts.ECKey.error")).onClose(this::hide).show();
}
});
}
private void addSuccessContent() {
removeContent();
closeButton.setVisible(false);
closeButton.setManaged(false);
headLineLabel.setText(Res.get("popup.accountSigning.successSingleAccount.success.headline"));
var descriptionLabel = addMultilineLabel(gridPane, ++rowIndex,
Res.get("popup.accountSigning.successSingleAccount.description",
Utilities.bytesAsHexString(selectedWitness.getHash())));
GridPane.setVgrow(descriptionLabel, Priority.ALWAYS);
GridPane.setValignment(descriptionLabel, VPos.TOP);
((AutoTooltipButton) actionButton).updateText(Res.get("shared.ok"));
actionButton.setOnAction(a -> hide());
}
@Override
protected void addButtons() {
var buttonTuple = add2ButtonsAfterGroup(gridPane, ++rowIndex + 1,
Res.get("popup.accountSigning.singleAccountSelect.headline"), Res.get("shared.cancel"));
actionButton = buttonTuple.first;
actionButton.setDisable(true);
actionButton.setOnAction(e -> selectAccountAgeWitness());
closeButton = (AutoTooltipButton) buttonTuple.second;
closeButton.setOnAction(e -> hide());
}
}

View File

@ -0,0 +1,196 @@
/*
* 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.desktop.main.overlays.windows;
import bisq.desktop.components.AutoTooltipButton;
import bisq.desktop.components.InputTextField;
import bisq.desktop.main.overlays.Overlay;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.core.account.sign.SignedWitness;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.locale.Res;
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import bisq.common.crypto.Hash;
import bisq.common.util.Tuple3;
import bisq.common.util.Utilities;
import org.bitcoinj.core.Utils;
import javax.inject.Inject;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.geometry.VPos;
import javafx.collections.FXCollections;
import javafx.util.Callback;
import java.util.ArrayList;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import static bisq.desktop.util.FormBuilder.add2ButtonsAfterGroup;
import static bisq.desktop.util.FormBuilder.addInputTextField;
import static bisq.desktop.util.FormBuilder.addTopLabelListView;
import static bisq.desktop.util.FormBuilder.removeRowsFromGridPane;
@Slf4j
public class SignUnsignedPubKeysWindow extends Overlay<SignUnsignedPubKeysWindow> {
private ListView<SignedWitness> unsignedPubKeys = new ListView<>();
private InputTextField privateKey;
private final AccountAgeWitnessService accountAgeWitnessService;
private final ArbitratorManager arbitratorManager;
private List<SignedWitness> signedWitnessList = new ArrayList<>();
private List<String> failed = new ArrayList<>();
private Callback<ListView<SignedWitness>, ListCell<SignedWitness>> signedWitnessCellFactory;
@Inject
public SignUnsignedPubKeysWindow(AccountAgeWitnessService accountAgeWitnessService,
ArbitratorManager arbitratorManager) {
this.accountAgeWitnessService = accountAgeWitnessService;
this.arbitratorManager = arbitratorManager;
signedWitnessCellFactory = new Callback<>() {
@Override
public ListCell<SignedWitness> call(
ListView<SignedWitness> param) {
return new ListCell<>() {
@Override
protected void updateItem(SignedWitness item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
setText(Utilities.bytesAsHexString(Hash.getRipemd160hash(item.getSignerPubKey())));
} else {
setText(null);
}
}
};
}
};
}
@Override
public void show() {
width = 1000;
rowIndex = -1;
createGridPane();
gridPane.setPrefHeight(600);
gridPane.getColumnConstraints().get(1).setHgrow(Priority.NEVER);
headLine(Res.get("popup.accountSigning.singleAccountSelect.headline"));
type = Type.Attention;
addHeadLine();
addUnsignedPubKeysContent();
addECKeyField();
addButtons();
applyStyles();
display();
}
private void addUnsignedPubKeysContent() {
Tuple3<Label, ListView<SignedWitness>, VBox> unsignedPubKeysTuple = addTopLabelListView(gridPane, ++rowIndex,
Res.get("popup.accountSigning.unsignedPubKeys.headline"));
unsignedPubKeys = unsignedPubKeysTuple.second;
unsignedPubKeys.setCellFactory(signedWitnessCellFactory);
unsignedPubKeys.setItems(FXCollections.observableArrayList(
accountAgeWitnessService.getUnsignedSignerPubKeys()));
}
private void addECKeyField() {
privateKey = addInputTextField(gridPane, ++rowIndex, Res.get("popup.accountSigning.signAccounts.ECKey"));
GridPane.setVgrow(privateKey, Priority.ALWAYS);
GridPane.setValignment(privateKey, VPos.TOP);
}
private void removeContent() {
removeRowsFromGridPane(gridPane, 1, 3);
rowIndex = 1;
}
private void signPubKeys() {
removeContent();
headLineLabel.setText(Res.get("popup.accountSigning.unsignedPubKeys.signed"));
var arbitratorKey = arbitratorManager.getRegistrationKey(privateKey.getText());
if (arbitratorKey != null) {
var arbitratorPubKeyAsHex = Utils.HEX.encode(arbitratorKey.getPubKey());
var isKeyValid = arbitratorManager.isPublicKeyInList(arbitratorPubKeyAsHex);
failed.clear();
if (isKeyValid) {
unsignedPubKeys.getItems().forEach(signedWitness -> {
var result = accountAgeWitnessService.arbitratorSignOrphanPubKey(arbitratorKey,
signedWitness.getSignerPubKey(), signedWitness.getDate());
if (result.isEmpty()) {
signedWitnessList.add(signedWitness);
} else {
failed.add("Signing pubkey " + Utilities.bytesAsHexString(Hash.getRipemd160hash(
signedWitness.getSignerPubKey())) + " failed with error " + result);
}
});
showResult();
}
} else {
new Popup().error(Res.get("popup.accountSigning.signAccounts.ECKey.error")).onClose(this::hide).show();
}
}
private void showResult() {
removeContent();
closeButton.setVisible(false);
closeButton.setManaged(false);
Tuple3<Label, ListView<SignedWitness>, VBox> signedTuple = addTopLabelListView(gridPane, ++rowIndex,
Res.get("popup.accountSigning.unsignedPubKeys.result.signed"));
ListView<SignedWitness> signedWitnessListView = signedTuple.second;
signedWitnessListView.setCellFactory(signedWitnessCellFactory);
signedWitnessListView.setItems(FXCollections.observableArrayList(signedWitnessList));
Tuple3<Label, ListView<String>, VBox> failedTuple = addTopLabelListView(gridPane, ++rowIndex,
Res.get("popup.accountSigning.unsignedPubKeys.result.failed"));
ListView<String> failedView = failedTuple.second;
failedView.setItems(FXCollections.observableArrayList(failed));
((AutoTooltipButton) actionButton).updateText(Res.get("shared.ok"));
actionButton.setOnAction(a -> hide());
}
@Override
protected void addButtons() {
var buttonTuple = add2ButtonsAfterGroup(gridPane, ++rowIndex + 1,
Res.get("popup.accountSigning.unsignedPubKeys.sign"), Res.get("shared.cancel"));
actionButton = buttonTuple.first;
actionButton.setDisable(unsignedPubKeys.getItems().size() == 0);
actionButton.setOnAction(e -> signPubKeys());
closeButton = (AutoTooltipButton) buttonTuple.second;
closeButton.setOnAction(e -> hide());
}
}

View File

@ -374,7 +374,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
checkNotNull(trade.getOffer(), "offer must not be null");
AccountAgeWitness myWitness = accountAgeWitnessService.getMyWitness(dataModel.getSellersPaymentAccountPayload());
accountAgeWitnessService.witnessDebugLog(trade, myWitness);
accountAgeWitnessService.getAccountAgeWitnessUtils().witnessDebugLog(trade, myWitness);
return accountAgeWitnessService.accountIsSigner(myWitness) &&
!accountAgeWitnessService.peerHasSignedWitness(trade) &&

View File

@ -23,6 +23,8 @@ import bisq.desktop.common.view.FxmlView;
import bisq.desktop.main.overlays.windows.ContractWindow;
import bisq.desktop.main.overlays.windows.DisputeSummaryWindow;
import bisq.desktop.main.overlays.windows.SignPaymentAccountsWindow;
import bisq.desktop.main.overlays.windows.SignSpecificWitnessWindow;
import bisq.desktop.main.overlays.windows.SignUnsignedPubKeysWindow;
import bisq.desktop.main.overlays.windows.TradeDetailsWindow;
import bisq.desktop.main.support.dispute.agent.DisputeAgentView;
@ -49,6 +51,8 @@ import javax.inject.Inject;
public class ArbitratorView extends DisputeAgentView {
private final SignPaymentAccountsWindow signPaymentAccountsWindow;
private final SignSpecificWitnessWindow signSpecificWitnessWindow;
private final SignUnsignedPubKeysWindow signUnsignedPubKeysWindow;
@Inject
public ArbitratorView(ArbitrationManager arbitrationManager,
@ -61,7 +65,9 @@ public class ArbitratorView extends DisputeAgentView {
TradeDetailsWindow tradeDetailsWindow,
AccountAgeWitnessService accountAgeWitnessService,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys,
SignPaymentAccountsWindow signPaymentAccountsWindow) {
SignPaymentAccountsWindow signPaymentAccountsWindow,
SignSpecificWitnessWindow signSpecificWitnessWindow,
SignUnsignedPubKeysWindow signUnsignedPubKeysWindow) {
super(arbitrationManager,
keyRing,
tradeManager,
@ -73,6 +79,8 @@ public class ArbitratorView extends DisputeAgentView {
accountAgeWitnessService,
useDevPrivilegeKeys);
this.signPaymentAccountsWindow = signPaymentAccountsWindow;
this.signSpecificWitnessWindow = signSpecificWitnessWindow;
this.signUnsignedPubKeysWindow = signUnsignedPubKeysWindow;
}
@Override
@ -89,6 +97,10 @@ public class ArbitratorView extends DisputeAgentView {
protected void handleKeyPressed(KeyEvent event) {
if (Utilities.isAltOrCtrlPressed(KeyCode.S, event)) {
signPaymentAccountsWindow.show();
} else if (Utilities.isAltOrCtrlPressed(KeyCode.P, event)) {
signSpecificWitnessWindow.show();
} else if (Utilities.isAltOrCtrlPressed(KeyCode.O, event)) {
signUnsignedPubKeysWindow.show();
}
}
}