minimum support for account witness verification & storage based on bisq

verify peer witness data and share with arbitrator for signing
arbitrator can sign witness hash, owner pub key (see developer-guide.md)
taker signs offer id nonce in SendOfferAvailabilityRequest
maker signs deposit tx hash in SendSignContractRequest
buyer verifies witness with payload on deposit confirmation
seller verifies witness with payload on payment sent message
add synchronization to User paymentAccountsAsObservable and elsewhere
This commit is contained in:
woodser 2023-01-09 15:32:24 -05:00
parent 963d92478d
commit 646380bc7a
28 changed files with 210 additions and 98 deletions

View File

@ -64,7 +64,7 @@ public class AccountAgeWitness implements ProcessOncePersistableNetworkPayload,
return protobuf.PersistableNetworkPayload.newBuilder().setAccountAgeWitness(builder).build();
}
protobuf.AccountAgeWitness toProtoAccountAgeWitness() {
public protobuf.AccountAgeWitness toProtoAccountAgeWitness() {
return toProtoMessage().getAccountAgeWitness();
}

View File

@ -531,6 +531,15 @@ public class AccountAgeWitnessService {
byte[] nonce,
byte[] signature,
ErrorMessageHandler errorMessageHandler) {
log.info("Verifying account age witness for {} {}, payment account payload hash={}, peers current date={}, nonce={}, signature={}",
trade.getClass().getSimpleName(),
trade.getId(),
Utilities.bytesAsHexString(peersPaymentAccountPayload.getHash()),
peersCurrentDate,
Utilities.bytesAsHexString(nonce),
Utilities.bytesAsHexString(signature));
final Optional<AccountAgeWitness> accountAgeWitnessOptional =
findWitness(peersPaymentAccountPayload, peersPubKeyRing);
// If we don't find a stored witness data we create a new dummy object which makes is easier to reuse the

View File

@ -31,6 +31,7 @@ import com.google.protobuf.ByteString;
import bisq.common.crypto.PubKeyRing;
import bisq.common.proto.network.NetworkPayload;
import bisq.common.util.JsonExclude;
import bisq.common.util.Utilities;
import org.bitcoinj.core.Coin;
@ -289,8 +290,8 @@ public final class Contract implements NetworkPayload {
",\n takerAccountId='" + takerAccountId + '\'' +
",\n makerPaymentMethodId='" + makerPaymentMethodId + '\'' +
",\n takerPaymentMethodId='" + takerPaymentMethodId + '\'' +
",\n makerPaymentAccountPayloadHash=" + makerPaymentAccountPayloadHash +
",\n takerPaymentAccountPayloadHash=" + takerPaymentAccountPayloadHash +
",\n makerPaymentAccountPayloadHash=" + Utilities.bytesAsHexString(makerPaymentAccountPayloadHash) +
",\n takerPaymentAccountPayloadHash=" + Utilities.bytesAsHexString(takerPaymentAccountPayloadHash) +
",\n makerPubKeyRing=" + makerPubKeyRing +
",\n takerPubKeyRing=" + takerPubKeyRing +
",\n makerPayoutAddressString='" + makerPayoutAddressString + '\'' +

View File

@ -1020,7 +1020,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
}
public Optional<Trade> getOpenTrade(String tradeId) {
return tradableList.stream().filter(e -> e.getId().equals(tradeId)).findFirst();
synchronized (tradableList) {
return tradableList.stream().filter(e -> e.getId().equals(tradeId)).findFirst();
}
}
public List<Trade> getOpenTrades() {

View File

@ -18,7 +18,7 @@
package bisq.core.trade.messages;
import bisq.core.account.sign.SignedWitness;
import bisq.core.account.witness.AccountAgeWitness;
import bisq.network.p2p.NodeAddress;
import bisq.common.app.Version;
@ -31,7 +31,6 @@ import java.util.UUID;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@ -49,32 +48,34 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
private final String signedPayoutTxHex;
private final String updatedMultisigHex;
private final boolean deferPublishPayout;
@Nullable
private final AccountAgeWitness buyerAccountAgeWitness;
@Nullable
private final SignedWitness buyerSignedWitness;
private final PaymentSentMessage paymentSentMessage;
@Setter
@Nullable
private byte[] sellerSignature;
// Added in v1.4.0
@Nullable
private final SignedWitness signedWitness;
public PaymentReceivedMessage(String tradeId,
NodeAddress senderNodeAddress,
@Nullable SignedWitness signedWitness,
String unsignedPayoutTxHex,
String signedPayoutTxHex,
String updatedMultisigHex,
boolean deferPublishPayout,
AccountAgeWitness buyerAccountAgeWitness,
@Nullable SignedWitness buyerSignedWitness,
PaymentSentMessage paymentSentMessage) {
this(tradeId,
senderNodeAddress,
signedWitness,
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
unsignedPayoutTxHex,
signedPayoutTxHex,
updatedMultisigHex,
deferPublishPayout,
buyerAccountAgeWitness,
buyerSignedWitness,
paymentSentMessage);
}
@ -85,22 +86,24 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
private PaymentReceivedMessage(String tradeId,
NodeAddress senderNodeAddress,
@Nullable SignedWitness signedWitness,
String uid,
String messageVersion,
String unsignedPayoutTxHex,
String signedPayoutTxHex,
String updatedMultisigHex,
boolean deferPublishPayout,
AccountAgeWitness buyerAccountAgeWitness,
@Nullable SignedWitness buyerSignedWitness,
PaymentSentMessage paymentSentMessage) {
super(messageVersion, tradeId, uid);
this.senderNodeAddress = senderNodeAddress;
this.signedWitness = signedWitness;
this.unsignedPayoutTxHex = unsignedPayoutTxHex;
this.signedPayoutTxHex = signedPayoutTxHex;
this.updatedMultisigHex = updatedMultisigHex;
this.deferPublishPayout = deferPublishPayout;
this.paymentSentMessage = paymentSentMessage;
this.buyerAccountAgeWitness = buyerAccountAgeWitness;
this.buyerSignedWitness = buyerSignedWitness;
}
@Override
@ -110,10 +113,11 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setUid(uid)
.setDeferPublishPayout(deferPublishPayout);
Optional.ofNullable(signedWitness).ifPresent(signedWitness -> builder.setSignedWitness(signedWitness.toProtoSignedWitness()));
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
Optional.ofNullable(unsignedPayoutTxHex).ifPresent(e -> builder.setUnsignedPayoutTxHex(unsignedPayoutTxHex));
Optional.ofNullable(signedPayoutTxHex).ifPresent(e -> builder.setSignedPayoutTxHex(signedPayoutTxHex));
Optional.ofNullable(buyerAccountAgeWitness).ifPresent(buyerAccountAgeWitness -> builder.setBuyerAccountAgeWitness(buyerAccountAgeWitness.toProtoAccountAgeWitness()));
Optional.ofNullable(buyerSignedWitness).ifPresent(buyerSignedWitness -> builder.setBuyerSignedWitness(buyerSignedWitness.toProtoSignedWitness()));
Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage()));
Optional.ofNullable(sellerSignature).ifPresent(e -> builder.setSellerSignature(ByteString.copyFrom(e)));
return getNetworkEnvelopeBuilder().setPaymentReceivedMessage(builder).build();
@ -121,20 +125,23 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
public static NetworkEnvelope fromProto(protobuf.PaymentReceivedMessage proto, String messageVersion) {
// There is no method to check for a nullable non-primitive data type object but we know that all fields
// are empty/null, so we check for the signature to see if we got a valid signedWitness.
protobuf.SignedWitness protoSignedWitness = proto.getSignedWitness();
SignedWitness signedWitness = !protoSignedWitness.getSignature().isEmpty() ?
// are empty/null, so we check for the signature to see if we got a valid buyerSignedWitness.
protobuf.AccountAgeWitness protoAccountAgeWitness = proto.getBuyerAccountAgeWitness();
AccountAgeWitness buyerAccountAgeWitness = protoAccountAgeWitness.getHash().isEmpty() ? null : AccountAgeWitness.fromProto(protoAccountAgeWitness);
protobuf.SignedWitness protoSignedWitness = proto.getBuyerSignedWitness();
SignedWitness buyerSignedWitness = !protoSignedWitness.getSignature().isEmpty() ?
SignedWitness.fromProto(protoSignedWitness) :
null;
PaymentReceivedMessage message = new PaymentReceivedMessage(proto.getTradeId(),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
signedWitness,
proto.getUid(),
messageVersion,
ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex()),
ProtoUtil.stringOrNullFromProto(proto.getSignedPayoutTxHex()),
ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()),
proto.getDeferPublishPayout(),
buyerAccountAgeWitness,
buyerSignedWitness,
proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion) : null);
message.setSellerSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getSellerSignature()));
return message;
@ -144,7 +151,7 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
public String toString() {
return "PaymentReceivedMessage{" +
"\n senderNodeAddress=" + senderNodeAddress +
",\n signedWitness=" + signedWitness +
",\n buyerSignedWitness=" + buyerSignedWitness +
",\n unsignedPayoutTxHex=" + unsignedPayoutTxHex +
",\n signedPayoutTxHex=" + signedPayoutTxHex +
",\n updatedMultisigHex=" + (updatedMultisigHex == null ? null : updatedMultisigHex.substring(0, Math.max(updatedMultisigHex.length(), 1000))) +

View File

@ -21,6 +21,7 @@ import bisq.network.p2p.NodeAddress;
import com.google.protobuf.ByteString;
import bisq.common.app.Version;
import bisq.common.proto.ProtoUtil;
import bisq.core.account.witness.AccountAgeWitness;
import java.util.Optional;
@ -42,6 +43,8 @@ public final class PaymentSentMessage extends TradeMailboxMessage {
private final String updatedMultisigHex;
@Nullable
private final byte[] paymentAccountKey;
@Nullable
private AccountAgeWitness sellerAccountAgeWitness;
@Setter
@Nullable
private byte[] buyerSignature;
@ -58,7 +61,8 @@ public final class PaymentSentMessage extends TradeMailboxMessage {
String uid,
@Nullable String signedPayoutTxHex,
@Nullable String updatedMultisigHex,
@Nullable byte[] paymentAccountKey) {
@Nullable byte[] paymentAccountKey,
AccountAgeWitness sellerAccountAgeWitness) {
this(tradeId,
senderNodeAddress,
counterCurrencyTxId,
@ -67,7 +71,8 @@ public final class PaymentSentMessage extends TradeMailboxMessage {
Version.getP2PMessageVersion(),
signedPayoutTxHex,
updatedMultisigHex,
paymentAccountKey);
paymentAccountKey,
sellerAccountAgeWitness);
}
@ -83,7 +88,8 @@ public final class PaymentSentMessage extends TradeMailboxMessage {
String messageVersion,
@Nullable String signedPayoutTxHex,
@Nullable String updatedMultisigHex,
@Nullable byte[] paymentAccountKey) {
@Nullable byte[] paymentAccountKey,
AccountAgeWitness sellerAccountAgeWitness) {
super(messageVersion, tradeId, uid);
this.senderNodeAddress = senderNodeAddress;
this.counterCurrencyTxId = counterCurrencyTxId;
@ -91,6 +97,7 @@ public final class PaymentSentMessage extends TradeMailboxMessage {
this.payoutTxHex = signedPayoutTxHex;
this.updatedMultisigHex = updatedMultisigHex;
this.paymentAccountKey = paymentAccountKey;
this.sellerAccountAgeWitness = sellerAccountAgeWitness;
}
@Override
@ -106,12 +113,17 @@ public final class PaymentSentMessage extends TradeMailboxMessage {
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
Optional.ofNullable(paymentAccountKey).ifPresent(e -> builder.setPaymentAccountKey(ByteString.copyFrom(e)));
Optional.ofNullable(buyerSignature).ifPresent(e -> builder.setBuyerSignature(ByteString.copyFrom(e)));
Optional.ofNullable(sellerAccountAgeWitness).ifPresent(e -> builder.setSellerAccountAgeWitness(sellerAccountAgeWitness.toProtoAccountAgeWitness()));
return getNetworkEnvelopeBuilder().setPaymentSentMessage(builder).build();
}
public static PaymentSentMessage fromProto(protobuf.PaymentSentMessage proto,
String messageVersion) {
protobuf.AccountAgeWitness protoAccountAgeWitness = proto.getSellerAccountAgeWitness();
AccountAgeWitness accountAgeWitness = protoAccountAgeWitness.getHash().isEmpty() ? null : AccountAgeWitness.fromProto(protoAccountAgeWitness);
PaymentSentMessage message = new PaymentSentMessage(proto.getTradeId(),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyTxId()),
@ -120,7 +132,8 @@ public final class PaymentSentMessage extends TradeMailboxMessage {
messageVersion,
ProtoUtil.stringOrNullFromProto(proto.getPayoutTxHex()),
ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()),
ProtoUtil.byteArrayOrNullFromProto(proto.getPaymentAccountKey())
ProtoUtil.byteArrayOrNullFromProto(proto.getPaymentAccountKey()),
accountAgeWitness
);
message.setBuyerSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getBuyerSignature()));
return message;

View File

@ -21,9 +21,15 @@ import bisq.core.proto.CoreProtoResolver;
import bisq.network.p2p.DirectMessage;
import bisq.network.p2p.NodeAddress;
import java.util.Optional;
import javax.annotation.Nullable;
import com.google.protobuf.ByteString;
import bisq.common.crypto.PubKeyRing;
import bisq.common.proto.ProtoUtil;
import bisq.common.util.Utilities;
import lombok.EqualsAndHashCode;
import lombok.Value;
@ -37,6 +43,7 @@ public final class SignContractRequest extends TradeMessage implements DirectMes
private final byte[] paymentAccountPayloadHash;
private final String payoutAddress;
private final String depositTxHash;
private final byte[] accountAgeWitnessSignatureOfDepositHash;
public SignContractRequest(String tradeId,
NodeAddress senderNodeAddress,
@ -47,7 +54,8 @@ public final class SignContractRequest extends TradeMessage implements DirectMes
String accountId,
byte[] paymentAccountPayloadHash,
String payoutAddress,
String depositTxHash) {
String depositTxHash,
@Nullable byte[] accountAgeWitnessSignatureOfDepositHash) {
super(messageVersion, tradeId, uid);
this.senderNodeAddress = senderNodeAddress;
this.pubKeyRing = pubKeyRing;
@ -56,6 +64,7 @@ public final class SignContractRequest extends TradeMessage implements DirectMes
this.paymentAccountPayloadHash = paymentAccountPayloadHash;
this.payoutAddress = payoutAddress;
this.depositTxHash = depositTxHash;
this.accountAgeWitnessSignatureOfDepositHash = accountAgeWitnessSignatureOfDepositHash;
}
@ -75,6 +84,7 @@ public final class SignContractRequest extends TradeMessage implements DirectMes
.setPayoutAddress(payoutAddress)
.setDepositTxHash(depositTxHash);
Optional.ofNullable(accountAgeWitnessSignatureOfDepositHash).ifPresent(e -> builder.setAccountAgeWitnessSignatureOfDepositHash(ByteString.copyFrom(e)));
builder.setCurrentDate(currentDate);
return getNetworkEnvelopeBuilder().setSignContractRequest(builder).build();
@ -92,7 +102,8 @@ public final class SignContractRequest extends TradeMessage implements DirectMes
proto.getAccountId(),
proto.getPaymentAccountPayloadHash().toByteArray(),
proto.getPayoutAddress(),
proto.getDepositTxHash());
proto.getDepositTxHash(),
ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignatureOfDepositHash()));
}
@Override
@ -102,9 +113,10 @@ public final class SignContractRequest extends TradeMessage implements DirectMes
",\n pubKeyRing=" + pubKeyRing +
",\n currentDate=" + currentDate +
",\n accountId=" + accountId +
",\n paymentAccountPayloadHash='" + paymentAccountPayloadHash +
",\n paymentAccountPayloadHash='" + Utilities.bytesAsHexString(paymentAccountPayloadHash) +
",\n payoutAddress='" + payoutAddress +
",\n depositTxHash='" + depositTxHash +
",\n accountAgeWitnessSignatureOfDepositHash='" + Utilities.bytesAsHexString(accountAgeWitnessSignatureOfDepositHash) +
"\n} " + super.toString();
}
}

View File

@ -22,19 +22,10 @@ import bisq.core.offer.Offer;
import bisq.core.trade.BuyerAsTakerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.handlers.TradeResultHandler;
import bisq.core.trade.messages.DepositResponse;
import bisq.core.trade.messages.InitMultisigRequest;
import bisq.core.trade.messages.DepositsConfirmedMessage;
import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.trade.messages.SignContractRequest;
import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.TakerReserveTradeFunds;
import bisq.core.trade.protocol.tasks.TakerSendInitTradeRequestToArbitrator;
import bisq.network.p2p.NodeAddress;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
import lombok.extern.slf4j.Slf4j;

View File

@ -37,6 +37,7 @@ import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.tasks.RemoveOffer;
import bisq.core.trade.protocol.tasks.ProcessPaymentSentMessage;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness;
import bisq.core.trade.protocol.FluentProtocol.Condition;
import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.MaybeSendSignContractRequest;
@ -387,7 +388,9 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
expect(new Condition(trade)
.with(response)
.from(sender))
.setup(tasks(ProcessDepositsConfirmedMessage.class)
.setup(tasks(
ProcessDepositsConfirmedMessage.class,
VerifyPeersAccountAgeWitness.class)
.using(new TradeTaskRunner(trade,
() -> {
handleTaskRunnerSuccess(sender, response);
@ -431,7 +434,8 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
}))
.setup(tasks(
ApplyFilter.class,
ProcessPaymentSentMessage.class)
ProcessPaymentSentMessage.class,
VerifyPeersAccountAgeWitness.class)
.using(new TradeTaskRunner(trade,
() -> {
handleTaskRunnerSuccess(peer, message);

View File

@ -17,6 +17,7 @@
package bisq.core.trade.protocol;
import bisq.core.account.witness.AccountAgeWitness;
import bisq.core.btc.model.RawTransactionInput;
import bisq.core.payment.payload.PaymentAccountPayload;
import bisq.core.proto.CoreProtoResolver;
@ -97,6 +98,10 @@ public final class TradingPeer implements PersistablePayload {
private byte[] accountAgeWitnessNonce;
@Nullable
private byte[] accountAgeWitnessSignature;
@Getter
@Setter
@Nullable
private AccountAgeWitness accountAgeWitness;
private long currentDate;
// Added in v.1.1.6
@ -155,6 +160,7 @@ public final class TradingPeer implements PersistablePayload {
Optional.ofNullable(changeOutputAddress).ifPresent(builder::setChangeOutputAddress);
Optional.ofNullable(accountAgeWitnessNonce).ifPresent(e -> builder.setAccountAgeWitnessNonce(ByteString.copyFrom(e)));
Optional.ofNullable(accountAgeWitnessSignature).ifPresent(e -> builder.setAccountAgeWitnessSignature(ByteString.copyFrom(e)));
Optional.ofNullable(accountAgeWitness).ifPresent(e -> builder.setAccountAgeWitness(accountAgeWitness.toProtoAccountAgeWitness()));
Optional.ofNullable(mediatedPayoutTxSignature).ifPresent(e -> builder.setMediatedPayoutTxSignature(ByteString.copyFrom(e)));
Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash));
Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex));
@ -203,6 +209,8 @@ public final class TradingPeer implements PersistablePayload {
tradingPeer.setChangeOutputAddress(ProtoUtil.stringOrNullFromProto(proto.getChangeOutputAddress()));
tradingPeer.setAccountAgeWitnessNonce(ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessNonce()));
tradingPeer.setAccountAgeWitnessSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignature()));
protobuf.AccountAgeWitness protoAccountAgeWitness = proto.getAccountAgeWitness();
tradingPeer.setAccountAgeWitness(protoAccountAgeWitness.getHash().isEmpty() ? null : AccountAgeWitness.fromProto(protoAccountAgeWitness));
tradingPeer.setCurrentDate(proto.getCurrentDate());
tradingPeer.setMediatedPayoutTxSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getMediatedPayoutTxSignature()));
tradingPeer.setReserveTxHash(ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()));

View File

@ -54,9 +54,6 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask {
runInterceptHook();
InitTradeRequest request = (InitTradeRequest) processModel.getTradeMessage();
// arbitrator signs offer id as nonce to avoid challenge protocol
byte[] sig = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), processModel.getOfferId().getBytes(Charsets.UTF_8));
// handle request from taker
if (request.getSenderNodeAddress().equals(trade.getTaker().getNodeAddress())) {
@ -73,7 +70,7 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask {
request.getPaymentMethodId(),
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
sig,
request.getAccountAgeWitnessSignatureOfOfferId(),
new Date().getTime(),
trade.getMaker().getNodeAddress(),
trade.getTaker().getNodeAddress(),

View File

@ -89,7 +89,8 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask
deterministicId,
trade.getPayoutTxHex(),
trade.getSelf().getUpdatedMultisigHex(),
trade.getSelf().getPaymentAccountKey()
trade.getSelf().getPaymentAccountKey(),
trade.getTradingPeer().getAccountAgeWitness()
);
// sign message

View File

@ -53,11 +53,8 @@ public class MakerSendInitTradeRequest extends TradeTask {
checkNotNull(makerRequest);
checkTradeId(processModel.getOfferId(), makerRequest);
// maker signs offer id as nonce to avoid challenge protocol // TODO: how is this used?
Offer offer = processModel.getOffer();
byte[] sig = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), offer.getId().getBytes(Charsets.UTF_8));
// create request to arbitrator
Offer offer = processModel.getOffer();
InitTradeRequest arbitratorRequest = new InitTradeRequest(
offer.getId(),
processModel.getMyNodeAddress(),
@ -70,7 +67,7 @@ public class MakerSendInitTradeRequest extends TradeTask {
offer.getOfferPayload().getPaymentMethodId(),
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
sig,
null,
makerRequest.getCurrentDate(),
trade.getMaker().getNodeAddress(),
trade.getTaker().getNodeAddress(),

View File

@ -18,17 +18,21 @@
package bisq.core.trade.protocol.tasks;
import bisq.common.app.Version;
import bisq.common.crypto.Sig;
import bisq.common.taskrunner.TaskRunner;
import bisq.core.btc.model.XmrAddressEntry;
import bisq.core.trade.ArbitratorTrade;
import bisq.core.trade.MakerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.Trade.State;
import bisq.core.trade.messages.SignContractRequest;
import bisq.network.p2p.SendDirectMessageListener;
import java.util.Date;
import java.util.UUID;
import com.google.common.base.Charsets;
import lombok.extern.slf4j.Slf4j;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroTxWallet;
// TODO (woodser): separate classes for deposit tx creation and contract request, or combine into ProcessInitMultisigRequest
@ -75,6 +79,12 @@ public class MaybeSendSignContractRequest extends TradeTask {
trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getAddressEntry(processModel.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString()); // TODO (woodser): allow custom payout address?
trade.getSelf().setPaymentAccountPayload(trade.getProcessModel().getPaymentAccountPayload(trade));
// maker signs deposit hash nonce to avoid challenge protocol
byte[] sig = null;
if (trade instanceof MakerTrade) {
sig = Sig.sign(processModel.getP2PService().getKeyRing().getSignatureKeyPair().getPrivate(), depositTx.getHash().getBytes(Charsets.UTF_8));
}
// create request for peer and arbitrator to sign contract
SignContractRequest request = new SignContractRequest(
trade.getOffer().getId(),
@ -86,7 +96,8 @@ public class MaybeSendSignContractRequest extends TradeTask {
trade.getProcessModel().getAccountId(),
trade.getSelf().getPaymentAccountPayload().getHash(),
trade.getSelf().getPayoutAddressString(),
depositTx.getHash());
depositTx.getHash(),
sig);
// send request to trading peer
processModel.getP2PService().sendEncryptedDirectMessage(trade.getTradingPeer().getNodeAddress(), trade.getTradingPeer().getPubKeyRing(), request, new SendDirectMessageListener() {

View File

@ -53,7 +53,7 @@ public class ProcessDepositsConfirmedMessage extends TradeTask {
// decrypt seller payment account payload if key given
if (request.getSellerPaymentAccountKey() != null && trade.getTradingPeer().getPaymentAccountPayload() == null) {
log.info(trade.getClass().getSimpleName() + " decryping using seller payment account key: " + request.getSellerPaymentAccountKey());
log.info(trade.getClass().getSimpleName() + " decrypting using seller payment account key");
trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey());
}

View File

@ -95,7 +95,7 @@ public class ProcessInitTradeRequest extends TradeTask {
}
}
// handle maker trade
// handle request as maker
else if (trade instanceof MakerTrade) {
multisigParticipant = processModel.getTaker();
trade.getTaker().setNodeAddress(request.getSenderNodeAddress()); // arbitrator sends maker InitTradeRequest with taker's node address and pub key ring

View File

@ -20,6 +20,7 @@ package bisq.core.trade.protocol.tasks;
import bisq.core.account.sign.SignedWitness;
import bisq.core.support.dispute.Dispute;
import bisq.core.trade.ArbitratorTrade;
import bisq.core.trade.BuyerTrade;
import bisq.core.trade.HavenoUtils;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.PaymentReceivedMessage;
@ -55,6 +56,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
HavenoUtils.verifyPaymentReceivedMessage(trade, message);
trade.getSeller().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
trade.getBuyer().setUpdatedMultisigHex(message.getPaymentSentMessage().getUpdatedMultisigHex());
trade.getBuyer().setAccountAgeWitness(message.getBuyerAccountAgeWitness());
// update to the latest peer address of our peer if message is correct
trade.getSeller().setNodeAddress(processModel.getTempTradingPeerNodeAddress());
@ -71,8 +73,8 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
// process payout tx unless already unlocked
if (!trade.isPayoutUnlocked()) processPayoutTx(message);
SignedWitness signedWitness = message.getSignedWitness();
if (signedWitness != null) {
SignedWitness signedWitness = message.getBuyerSignedWitness();
if (signedWitness != null && trade instanceof BuyerTrade) {
// We received the signedWitness from the seller and publish the data to the network.
// The signer has published it as well but we prefer to re-do it on our side as well to achieve higher
// resilience.

View File

@ -48,6 +48,7 @@ public class ProcessPaymentSentMessage extends TradeTask {
trade.setPayoutTxHex(message.getPayoutTxHex());
trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
trade.getBuyer().setPaymentSentMessage(message);
trade.getSeller().setAccountAgeWitness(message.getSellerAccountAgeWitness());
// if seller, decrypt buyer's payment account payload
if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());

View File

@ -37,9 +37,13 @@ import bisq.core.trade.protocol.TradingPeer;
import bisq.core.util.JsonUtil;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.SendDirectMessageListener;
import java.util.Date;
import java.util.UUID;
import javax.crypto.SecretKey;
import com.google.common.base.Charsets;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@ -66,6 +70,12 @@ public class ProcessSignContractRequest extends TradeTask {
trader.setAccountId(request.getAccountId());
trader.setPaymentAccountPayloadHash(request.getPaymentAccountPayloadHash());
trader.setPayoutAddressString(request.getPayoutAddress());
// maker sends witness signature of deposit tx hash
if (trader == trade.getMaker()) {
trader.setAccountAgeWitnessNonce(request.getDepositTxHash().getBytes(Charsets.UTF_8));
trader.setAccountAgeWitnessSignature(request.getAccountAgeWitnessSignatureOfDepositHash());
}
// sign contract only when both deposit txs hashes known
// TODO (woodser): remove makerDepositTxId and takerDepositTxId from Trade

View File

@ -18,6 +18,7 @@
package bisq.core.trade.protocol.tasks;
import bisq.core.account.sign.SignedWitness;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.trade.messages.TradeMailboxMessage;
@ -63,22 +64,23 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
checkNotNull(trade.getPayoutTxHex(), "Payout tx must not be null");
if (message == null) {
// TODO: sign witness
// AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService();
// if (accountAgeWitnessService.isSignWitnessTrade(trade)) {
// // Broadcast is done in accountAgeWitness domain.
// accountAgeWitnessService.traderSignAndPublishPeersAccountAgeWitness(trade).ifPresent(witness -> signedWitness = witness);
// }
// sign account witness
AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService();
if (accountAgeWitnessService.isSignWitnessTrade(trade)) {
accountAgeWitnessService.traderSignAndPublishPeersAccountAgeWitness(trade).ifPresent(witness -> signedWitness = witness);
log.info("{} {} signed and published peers account age witness", trade.getClass().getSimpleName(), trade.getId());
}
// TODO: create with deterministic id like BuyerSendPaymentSentMessage
message = new PaymentReceivedMessage(
tradeId,
processModel.getMyNodeAddress(),
signedWitness,
trade.isPayoutPublished() ? null : trade.getPayoutTxHex(), // unsigned
trade.isPayoutPublished() ? trade.getPayoutTxHex() : null, // signed
trade.getSelf().getUpdatedMultisigHex(),
trade.getState().ordinal() >= Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG.ordinal(), // informs to expect payout
trade.getTradingPeer().getAccountAgeWitness(),
signedWitness,
trade.getBuyer().getPaymentSentMessage()
);

View File

@ -21,6 +21,7 @@ import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.locale.CurrencyUtil;
import bisq.core.offer.Offer;
import bisq.core.payment.payload.PaymentAccountPayload;
import bisq.core.trade.ArbitratorTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.protocol.TradingPeer;
@ -46,14 +47,27 @@ public class VerifyPeersAccountAgeWitness extends TradeTask {
try {
runInterceptHook();
// only verify fiat offer
Offer offer = checkNotNull(trade.getOffer());
if (CurrencyUtil.isCryptoCurrency(offer.getCurrencyCode())) {
complete();
return;
}
AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService();
// skip if arbitrator
if (trade instanceof ArbitratorTrade) {
complete();
return;
}
// skip if payment account payload is null
TradingPeer tradingPeer = trade.getTradingPeer();
if (tradingPeer.getPaymentAccountPayload() == null) {
complete();
return;
}
AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService();
PaymentAccountPayload peersPaymentAccountPayload = checkNotNull(tradingPeer.getPaymentAccountPayload(),
"Peers peersPaymentAccountPayload must not be null");
PubKeyRing peersPubKeyRing = checkNotNull(tradingPeer.getPubKeyRing(), "peersPubKeyRing must not be null");
@ -71,6 +85,8 @@ public class VerifyPeersAccountAgeWitness extends TradeTask {
signature,
errorMsg::set);
if (isValid) {
trade.getTradingPeer().setAccountAgeWitness(processModel.getAccountAgeWitnessService().findWitness(trade.getTradingPeer().getPaymentAccountPayload(), trade.getTradingPeer().getPubKeyRing()).orElse(null));
log.info("{} {} verified witness data of peer {}", trade.getClass().getSimpleName(), trade.getId(), tradingPeer.getNodeAddress());
complete();
} else {
failed(errorMsg.get());

View File

@ -119,8 +119,10 @@ public class User implements PersistedDataHost {
userPayload.getAcceptedLanguageLocaleCodes().add(english);
paymentAccountsAsObservable.addListener((SetChangeListener<PaymentAccount>) change -> {
userPayload.setPaymentAccounts(new HashSet<>(paymentAccountsAsObservable));
requestPersistence();
synchronized (paymentAccountsAsObservable) {
userPayload.setPaymentAccounts(new HashSet<>(paymentAccountsAsObservable));
requestPersistence();
}
});
currentPaymentAccountProperty.addListener((ov) -> {
userPayload.setCurrentPaymentAccount(currentPaymentAccountProperty.get());
@ -212,28 +214,33 @@ public class User implements PersistedDataHost {
public void addPaymentAccount(PaymentAccount paymentAccount) {
paymentAccount.onAddToUser();
boolean changed = paymentAccountsAsObservable.add(paymentAccount);
setCurrentPaymentAccount(paymentAccount);
if (changed)
requestPersistence();
synchronized (paymentAccountsAsObservable) {
boolean changed = paymentAccountsAsObservable.add(paymentAccount);
setCurrentPaymentAccount(paymentAccount);
if (changed)
requestPersistence();
}
}
public void addImportedPaymentAccounts(Collection<PaymentAccount> paymentAccounts) {
isPaymentAccountImport = true;
synchronized (paymentAccountsAsObservable) {
isPaymentAccountImport = true;
boolean changed = paymentAccountsAsObservable.addAll(paymentAccounts);
paymentAccounts.stream().findFirst().ifPresent(this::setCurrentPaymentAccount);
if (changed)
requestPersistence();
isPaymentAccountImport = false;
boolean changed = paymentAccountsAsObservable.addAll(paymentAccounts);
paymentAccounts.stream().findFirst().ifPresent(this::setCurrentPaymentAccount);
if (changed)
requestPersistence();
isPaymentAccountImport = false;
}
}
public void removePaymentAccount(PaymentAccount paymentAccount) {
boolean changed = paymentAccountsAsObservable.remove(paymentAccount);
if (changed)
requestPersistence();
synchronized (paymentAccountsAsObservable) {
boolean changed = paymentAccountsAsObservable.remove(paymentAccount);
if (changed)
requestPersistence();
}
}
public boolean addAcceptedArbitrator(Arbitrator arbitrator) {
@ -513,7 +520,9 @@ public class User implements PersistedDataHost {
}
private boolean paymentAccountExists(PaymentAccount paymentAccount) {
return getPaymentAccountsAsObservable().stream().anyMatch(e -> e.equals(paymentAccount));
synchronized (paymentAccountsAsObservable) {
return getPaymentAccountsAsObservable().stream().anyMatch(e -> e.equals(paymentAccount));
}
}
public Cookie getCookie() {

View File

@ -523,7 +523,7 @@ public class PopOver extends PopupControl {
skinNode);
fadeOut.setFromValue(skinNode.getOpacity());
fadeOut.setToValue(0);
fadeOut.setOnFinished(evt -> super.hide());
fadeOut.setOnFinished(evt -> { if (super.isShowing()) super.hide(); });
fadeOut.play();
} else {
super.hide();

View File

@ -200,9 +200,7 @@ public class SignPaymentAccountsWindow extends Overlay<SignPaymentAccountsWindow
++rowIndex, Res.get("popup.accountSigning.confirmSelectedAccounts.headline"));
GridPane.setRowSpan(selectedPaymentAccountsTuple.third, 2);
selectedPaymentAccountsList = selectedPaymentAccountsTuple.second;
ObservableList<Dispute> disputesAsObservableList = useDevPrivilegeKeys ?
mediationManager.getDisputesAsObservableList()
: arbitrationManager.getDisputesAsObservableList();
ObservableList<Dispute> disputesAsObservableList = arbitrationManager.getDisputesAsObservableList();
long safeDate = datePicker.getValue().atStartOfDay().toEpochSecond(ZoneOffset.UTC) * 1000;
List<TraderDataItem> traderDataItemList;
StringBuilder sb = new StringBuilder("Summary for ").append(appName).append("\n");

View File

@ -43,6 +43,7 @@ import bisq.network.p2p.NodeAddress;
import bisq.common.UserThread;
import bisq.common.util.Tuple3;
import bisq.common.util.Utilities;
import javax.inject.Inject;
import javax.inject.Named;
@ -313,6 +314,11 @@ public class TradeDetailsWindow extends Overlay<TradeDetailsWindow> {
HBox.setHgrow(spacer, Priority.ALWAYS);
hBox.getChildren().add(0, spacer);
String buyerWitnessHash = trade.getBuyer().getAccountAgeWitness() == null ? "null" : Utilities.bytesAsHexString(trade.getBuyer().getAccountAgeWitness().getHash());
String buyerPubKeyRingHash = Utilities.bytesAsHexString(trade.getBuyer().getPubKeyRing().getSignaturePubKeyBytes());
String sellerWitnessHash = trade.getSeller().getAccountAgeWitness() == null ? "null" : Utilities.bytesAsHexString(trade.getSeller().getAccountAgeWitness().getHash());
String sellerPubKeyRingHash = Utilities.bytesAsHexString(trade.getSeller().getPubKeyRing().getSignaturePubKeyBytes());
if (contract != null) {
viewContractButton.setOnAction(e -> {
TextArea textArea = new HavenoTextArea();
@ -321,8 +327,10 @@ public class TradeDetailsWindow extends Overlay<TradeDetailsWindow> {
data += trade.getContractAsJson();
data += "\n\nOther detail data:";
if (offer.isFiatOffer()) {
data += "\n\nBuyersAccountAge: " + buyersAccountAge;
data += "\nSellersAccountAge: " + sellersAccountAge;
data += "\n\nBuyers witness hash,pub key ring hash: " + buyerWitnessHash + "," + buyerPubKeyRingHash;
data += "\nBuyers account age: " + buyersAccountAge;
data += "\nSellers witness hash,pub key ring hash: " + sellerWitnessHash + "," + sellerPubKeyRingHash;
data += "\nSellers account age: " + sellersAccountAge;
}
// TODO (woodser): include maker and taker deposit tx hex in contract?

View File

@ -448,7 +448,7 @@ public class SellerStep3View extends TradeStepView {
}
if (model.dataModel.isSignWitnessTrade()) {
message += Res.get("portfolio.pending.step3_seller.onPaymentReceived.signer");
message += "\n\n" + Res.get("portfolio.pending.step3_seller.onPaymentReceived.signer");
}
}
if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) {

View File

@ -34,6 +34,15 @@ Follow [instructions](https://github.com/haveno-dex/haveno-ts#run-tests) to run
9. Run the tests with `npm run test -- -t 'my test'` to run tests by name and `npm test` to run all tests together. Ensure all tests pass and there are no exception stacktraces in the terminals of Alice, Bob, or the arbitrator.
10. Open pull requests to the haveno and haveno-ts projects for the backend and frontend implementations.
## How to manually sign accounts as the arbitrator
1. Open legacy UI as the arbitrator.
2. Go to the 'Account' tab.
3. Open Signing tab: `ctrl+i`
a. Sign payment account: `ctrl+s`, select payment accounts to sign (sourced from disputes).
b. Sign account age witness: `ctrl+p` then enter <witness hash>,<pub key hash> (from past trade details) and click the "Import unsigned account age witness" button.
c. Sign unsigned witness pub keys: `ctrl+o`
## How to rebase and squash your commits
When submitting a pull request for review, please first rebase and squash your commits.

View File

@ -312,6 +312,7 @@ message SignContractRequest {
bytes payment_account_payload_hash = 7;
string payout_address = 8;;
string deposit_tx_hash = 9;
bytes account_age_witness_signature_of_deposit_hash = 10;
}
message SignContractResponse {
@ -404,20 +405,22 @@ message PaymentSentMessage {
string payout_tx_hex = 6;
string updated_multisig_hex = 7;
bytes payment_account_key = 8;
bytes buyer_signature = 9;
AccountAgeWitness seller_account_age_witness = 9;
bytes buyer_signature = 10;
}
message PaymentReceivedMessage {
string trade_id = 1;
NodeAddress sender_node_address = 2;
string uid = 3;
SignedWitness signed_witness = 4; // Added in v1.4.0
string unsigned_payout_tx_hex = 5;
string signed_payout_tx_hex = 6;
string updated_multisig_hex = 7;
bool defer_publish_payout = 8;
PaymentSentMessage payment_sent_message = 9;
bytes seller_signature = 10;
string unsigned_payout_tx_hex = 4;
string signed_payout_tx_hex = 5;
string updated_multisig_hex = 6;
bool defer_publish_payout = 7;
AccountAgeWitness buyer_account_age_witness = 8;
SignedWitness buyer_signed_witness = 9;
PaymentSentMessage payment_sent_message = 10;
bytes seller_signature = 11;
}
message MediatedPayoutTxPublishedMessage {
@ -1732,8 +1735,9 @@ message TradingPeer {
string change_output_address = 17;
bytes account_age_witness_nonce = 18;
bytes account_age_witness_signature = 19;
int64 current_date = 20;
bytes mediated_payout_tx_signature = 21;
AccountAgeWitness account_age_witness = 20;
int64 current_date = 21;
bytes mediated_payout_tx_signature = 22;
string reserve_tx_hash = 1001;
string reserve_tx_hex = 1002;