implement silent payments support in wallet with psbt output field

This commit is contained in:
Craig Raw 2025-09-29 08:34:38 +02:00
parent af879a30f1
commit a896809286
17 changed files with 577 additions and 221 deletions

View file

@ -0,0 +1,40 @@
package com.sparrowwallet.drongo.dns;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import java.util.Objects;
public class DnsAddress {
private final Address address;
private final SilentPaymentAddress silentPaymentAddress;
public DnsAddress(Address address) {
this.address = address;
this.silentPaymentAddress = null;
}
public DnsAddress(SilentPaymentAddress silentPaymentAddress) {
this.address = null;
this.silentPaymentAddress = silentPaymentAddress;
}
@Override
public final boolean equals(Object o) {
if(this == o) {
return true;
}
if(!(o instanceof DnsAddress that)) {
return false;
}
return Objects.equals(address, that.address) && Objects.equals(silentPaymentAddress, that.silentPaymentAddress);
}
@Override
public int hashCode() {
int result = Objects.hashCode(address);
result = 31 * result + Objects.hashCode(silentPaymentAddress);
return result;
}
}

View file

@ -25,4 +25,12 @@ public record DnsPayment(String hrn, BitcoinURI bitcoinURI, byte[] proofChain) {
return ttl;
}
public boolean hasAddress() {
return bitcoinURI.getAddress() != null;
}
public boolean hasSilentPaymentAddress() {
return bitcoinURI.getSilentPaymentAddress() != null;
}
}

View file

@ -4,6 +4,9 @@ import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Expiry;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.wallet.Payment;
import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull;
@ -13,19 +16,19 @@ public class DnsPaymentCache {
public static final long MAX_TTL_SECONDS = 604800L;
public static final long MIN_TTL_SECONDS = 1800L;
private static final Cache<@NonNull Address, @NonNull DnsPayment> dnsPayments = Caffeine.newBuilder().expireAfter(new Expiry<@NonNull Address, @NonNull DnsPayment>() {
private static final Cache<@NonNull DnsAddress, @NonNull DnsPayment> dnsPayments = Caffeine.newBuilder().expireAfter(new Expiry<@NonNull DnsAddress, @NonNull DnsPayment>() {
@Override
public long expireAfterCreate(@NonNull Address address, @NonNull DnsPayment dnsPayment, long currentTime) {
public long expireAfterCreate(@NonNull DnsAddress address, @NonNull DnsPayment dnsPayment, long currentTime) {
return TimeUnit.SECONDS.toNanos(Math.max(dnsPayment.getTTL(), MIN_TTL_SECONDS));
}
@Override
public long expireAfterUpdate(@NonNull Address address, @NonNull DnsPayment dnsPayment, long currentTime, @NonNegative long currentDuration) {
public long expireAfterUpdate(@NonNull DnsAddress address, @NonNull DnsPayment dnsPayment, long currentTime, @NonNegative long currentDuration) {
return expireAfterCreate(address, dnsPayment, currentTime);
}
@Override
public long expireAfterRead(@NonNull Address address, @NonNull DnsPayment dnsPayment, long currentTime, @NonNegative long currentDuration) {
public long expireAfterRead(@NonNull DnsAddress address, @NonNull DnsPayment dnsPayment, long currentTime, @NonNegative long currentDuration) {
return currentDuration;
}
}).build();
@ -33,10 +36,26 @@ public class DnsPaymentCache {
private DnsPaymentCache() {}
public static DnsPayment getDnsPayment(Address address) {
return dnsPayments.getIfPresent(address);
return dnsPayments.getIfPresent(new DnsAddress(address));
}
public static DnsPayment getDnsPayment(SilentPaymentAddress silentPaymentAddress) {
return dnsPayments.getIfPresent(new DnsAddress(silentPaymentAddress));
}
public static DnsPayment getDnsPayment(Payment payment) {
if(payment instanceof SilentPayment silentPayment) {
return dnsPayments.getIfPresent(new DnsAddress(silentPayment.getSilentPaymentAddress()));
} else {
return dnsPayments.getIfPresent(new DnsAddress(payment.getAddress()));
}
}
public static void putDnsPayment(Address address, DnsPayment dnsPayment) {
dnsPayments.put(address, dnsPayment);
dnsPayments.put(new DnsAddress(address), dnsPayment);
}
public static void putDnsPayment(SilentPaymentAddress silentPaymentAddress, DnsPayment dnsPayment) {
dnsPayments.put(new DnsAddress(silentPaymentAddress), dnsPayment);
}
}

View file

@ -373,8 +373,8 @@ public class Transaction extends ChildMessage {
return Collections.unmodifiableList(outputs);
}
public void shuffleOutputs() {
Collections.shuffle(outputs);
public void swapOutputs(int i, int j) {
Collections.swap(outputs, i, j);
}
public TransactionOutput addOutput(long value, Script script) {

View file

@ -7,6 +7,7 @@ import java.io.OutputStream;
public class TransactionInput extends ChildMessage {
public static final long SEQUENCE_LOCKTIME_DISABLED = 4294967295L;
public static final long SEQUENCE_RBF_DISABLED = 4294967294L;
public static final long SEQUENCE_RBF_ENABLED = 4294967293L;
public static final long MAX_RELATIVE_TIMELOCK = 2147483647L;
public static final long RELATIVE_TIMELOCK_VALUE_MASK = 0xFFFF;

View file

@ -60,6 +60,20 @@ public class TransactionOutput extends ChildMessage {
return scriptBytes;
}
public void setScriptBytes(byte[] scriptBytes) {
super.payload = null;
this.script = null;
int oldLength = length;
this.scriptBytes = scriptBytes;
// 8 = value
int newLength = 8 + (scriptBytes == null ? 1 : VarInt.sizeOf(scriptBytes.length) + scriptBytes.length);
adjustLength(newLength - oldLength);
}
public void clearScriptBytes() {
setScriptBytes(new byte[0]);
}
public Script getScript() {
if(script == null) {
script = new Script(scriptBytes);

View file

@ -3,7 +3,6 @@ package com.sparrowwallet.drongo.psbt;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.wallet.*;
@ -81,6 +80,7 @@ public class PSBT {
Wallet wallet = walletTransaction.getWallet();
transaction = new Transaction(walletTransaction.getTransaction().bitcoinSerialize());
List<WalletTransaction.Output> walletTransactionOutputs = new ArrayList<>(walletTransaction.getOutputs());
//Clear segwit marker & flag, scriptSigs and all witness data as per BIP174
transaction.clearSegwit();
@ -90,7 +90,12 @@ public class PSBT {
}
//Shuffle outputs so change outputs are less obvious
transaction.shuffleOutputs();
Random random = new Random();
for(int i = transaction.getOutputs().size() - 1; i > 0; i--) {
int j = random.nextInt(i + 1);
transaction.swapOutputs(i, j);
Collections.swap(walletTransactionOutputs, i, j);
}
if(includeGlobalXpubs) {
for(Keystore keystore : walletTransaction.getWallet().getKeystores()) {
@ -143,30 +148,15 @@ public class PSBT {
psbtInputs.add(psbtInput);
}
List<WalletNode> outputNodes = new ArrayList<>();
List<Map<String, byte[]>> dnsProofs = new ArrayList<>();
for(TransactionOutput txOutput : transaction.getOutputs()) {
try {
Address address = txOutput.getScript().getToAddresses()[0];
if(walletTransaction.getAddressNodeMap().containsKey(address)) {
outputNodes.add(walletTransaction.getAddressNodeMap().get(address));
} else if(walletTransaction.getChangeMap().keySet().stream().anyMatch(changeNode -> changeNode.getAddress().equals(address))) {
outputNodes.add(walletTransaction.getChangeMap().keySet().stream().filter(changeNode -> changeNode.getAddress().equals(address)).findFirst().orElse(null));
}
dnsProofs.add(walletTransaction.getAddressDnsProofMap().get(address));
} catch(NonStandardScriptException e) {
//Ignore, likely OP_RETURN output
outputNodes.add(null);
dnsProofs.add(null);
}
}
for(int outputIndex = 0; outputIndex < outputNodes.size(); outputIndex++) {
WalletNode outputNode = outputNodes.get(outputIndex);
if(outputNode == null) {
PSBTOutput externalRecipientOutput = new PSBTOutput(this, outputIndex, null, null, null, Collections.emptyMap(), Collections.emptyMap(), null, dnsProofs.get(outputIndex));
psbtOutputs.add(externalRecipientOutput);
} else {
for(int outputIndex = 0; outputIndex < walletTransactionOutputs.size(); outputIndex++) {
WalletTransaction.Output output = walletTransactionOutputs.get(outputIndex);
PSBTOutput psbtOutput;
if(output instanceof WalletTransaction.SilentPaymentOutput silentPaymentOutput) {
psbtOutput = new PSBTOutput(this, outputIndex, null, null, null, Collections.emptyMap(), Collections.emptyMap(), null, silentPaymentOutput.getSilentPayment().getSilentPaymentAddress(), silentPaymentOutput.getDnsSecProof());
} else if(output instanceof WalletTransaction.PaymentOutput paymentOutput) {
psbtOutput = new PSBTOutput(this, outputIndex, null, null, null, Collections.emptyMap(), Collections.emptyMap(), null, null, paymentOutput.getDnsSecProof());
} else if(output instanceof WalletTransaction.ChangeOutput changeOutput) {
WalletNode outputNode = changeOutput.getWalletNode();
TransactionOutput txOutput = transaction.getOutputs().get(outputIndex);
Wallet recipientWallet = outputNode.getWallet();
@ -195,9 +185,11 @@ public class PSBT {
}
}
PSBTOutput walletOutput = new PSBTOutput(this, outputIndex, recipientWallet.getScriptType(), redeemScript, witnessScript, derivedPublicKeys, Collections.emptyMap(), tapInternalKey, null);
psbtOutputs.add(walletOutput);
psbtOutput = new PSBTOutput(this, outputIndex, recipientWallet.getScriptType(), redeemScript, witnessScript, derivedPublicKeys, Collections.emptyMap(), tapInternalKey, null, null);
} else {
psbtOutput = new PSBTOutput(this, outputIndex, null, null, null, Collections.emptyMap(), Collections.emptyMap(), null, null, null);
}
psbtOutputs.add(psbtOutput);
}
}
@ -478,8 +470,8 @@ public class PSBT {
if(output.amount() == null) {
throw new PSBTParseException("PSBT_OUT_AMOUNT is required in PSBTv2");
}
if(output.script() == null) {
throw new PSBTParseException("PSBT_OUT_SCRIPT is required in PSBTv2");
if(output.script() == null && output.getSilentPaymentAddress() == null) {
throw new PSBTParseException("Either PSBT_OUT_SCRIPT or PSBT_OUT_SP_V0_INFO is required in PSBTv2");
}
}
@ -811,7 +803,7 @@ public class PSBT {
}
}
for(PSBTOutput psbtOutput : getPsbtOutputs()) {
transaction.addOutput(psbtOutput.getAmount(), psbtOutput.getScript());
transaction.addOutput(psbtOutput.getAmount(), psbtOutput.getScript() == null ? new Script(new byte[0]) : psbtOutput.getScript());
}
return transaction;
}
@ -996,6 +988,29 @@ public class PSBT {
return maxHeightLocktime.orElse(maxTimeLocktime.orElse(fallback));
}
public PSBT getForExport() {
//If this PSBT contains silent payment outputs, it must be converted to PSBTv2
if(getPsbtOutputs().stream().anyMatch(psbtOutput -> psbtOutput.getSilentPaymentAddress() != null)) {
try {
PSBT psbt2 = new PSBT(serialize());
//PSBTv0 serialized outputs do not contain silent payment info, so copy over
for(int i = 0; i < getPsbtOutputs().size(); i++) {
PSBTOutput psbtOutput = getPsbtOutputs().get(i);
PSBTOutput psbtOutput2 = psbt2.getPsbtOutputs().get(i);
psbtOutput2.setSilentPaymentAddress(psbtOutput.getSilentPaymentAddress());
}
if(psbt2.getVersion() == null || psbt2.getVersion() == 0) {
psbt2.convertVersion(2);
return psbt2;
}
} catch(PSBTParseException e) {
throw new IllegalArgumentException("Could not reparse PSBT", e);
}
}
return this;
}
public static boolean isPSBT(byte[] b) {
try {
ByteBuffer buffer = ByteBuffer.wrap(b);

View file

@ -7,6 +7,7 @@ import com.sparrowwallet.drongo.dns.DnsPayment;
import com.sparrowwallet.drongo.dns.DnsPaymentResolver;
import com.sparrowwallet.drongo.dns.DnsPaymentValidationException;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.uri.BitcoinURIParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -26,6 +27,7 @@ public class PSBTOutput {
public static final byte PSBT_OUT_SCRIPT = 0x04;
public static final byte PSBT_OUT_TAP_INTERNAL_KEY = 0x05;
public static final byte PSBT_OUT_TAP_BIP32_DERIVATION = 0x07;
public static final byte PSBT_OUT_SP_V0_INFO = 0x09;
public static final byte PSBT_OUT_DNSSEC_PROOF = 0x35;
public static final byte PSBT_OUT_PROPRIETARY = (byte)0xfc;
@ -40,6 +42,7 @@ public class PSBTOutput {
//PSBTv2 fields
private Long amount;
private Script script;
private SilentPaymentAddress silentPaymentAddress;
private static final Logger log = LoggerFactory.getLogger(PSBTOutput.class);
@ -52,7 +55,7 @@ public class PSBTOutput {
}
PSBTOutput(PSBT psbt, int index, ScriptType scriptType, Script redeemScript, Script witnessScript, Map<ECKey, KeyDerivation> derivedPublicKeys,
Map<String, String> proprietary, ECKey tapInternalKey, Map<String, byte[]> dnssecProof) {
Map<String, String> proprietary, ECKey tapInternalKey, SilentPaymentAddress silentPaymentAddress, Map<String, byte[]> dnssecProof) {
this(psbt, index);
this.redeemScript = redeemScript;
@ -71,6 +74,7 @@ public class PSBTOutput {
tapDerivedPublicKeys.put(this.tapInternalKey, Map.of(tapKeyDerivation, Collections.emptyList()));
}
this.silentPaymentAddress = silentPaymentAddress;
this.dnssecProof = dnssecProof;
}
@ -130,6 +134,16 @@ public class PSBTOutput {
}
}
break;
case PSBT_OUT_SP_V0_INFO:
entry.checkOneByteKey();
if(entry.getData().length != 66) {
throw new PSBTParseException("PSBT output info data for silent payments address must contain 66 bytes");
}
byte[] scanKey = new byte[33];
System.arraycopy(entry.getData(), 0, scanKey, 0, 33);
byte[] spendKey = new byte[33];
System.arraycopy(entry.getData(), 33, spendKey, 0, 33);
this.silentPaymentAddress = new SilentPaymentAddress(ECKey.fromPublicOnly(scanKey), ECKey.fromPublicOnly(spendKey));
case PSBT_OUT_DNSSEC_PROOF:
entry.checkOneByteKey();
this.dnssecProof = parseDnssecProof(entry.getData());
@ -164,6 +178,9 @@ public class PSBTOutput {
if(script != null) {
entries.add(populateEntry(PSBT_OUT_SCRIPT, null, script.getProgram()));
}
if(silentPaymentAddress != null) {
entries.add(populateEntry(PSBT_OUT_SP_V0_INFO, null, Utils.concat(silentPaymentAddress.getScanKey().getPubKey(), silentPaymentAddress.getSpendKey().getPubKey())));
}
}
for(Map.Entry<String, String> entry : proprietary.entrySet()) {
@ -291,6 +308,14 @@ public class PSBTOutput {
this.tapInternalKey = tapInternalKey;
}
public SilentPaymentAddress getSilentPaymentAddress() {
return silentPaymentAddress;
}
public void setSilentPaymentAddress(SilentPaymentAddress silentPaymentAddress) {
this.silentPaymentAddress = silentPaymentAddress;
}
public Map<String, byte[]> getDnssecProof() {
return dnssecProof;
}

View file

@ -0,0 +1,11 @@
package com.sparrowwallet.drongo.silentpayments;
public class InvalidSilentPaymentException extends Exception {
public InvalidSilentPaymentException(String message) {
super(message);
}
public InvalidSilentPaymentException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -1,6 +1,5 @@
package com.sparrowwallet.drongo.silentpayments;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.P2TRAddress;
import com.sparrowwallet.drongo.protocol.ScriptType;
@ -14,15 +13,32 @@ public class SilentPayment extends Payment {
private final SilentPaymentAddress silentPaymentAddress;
public SilentPayment(SilentPaymentAddress silentPaymentAddress, String label, long amount, boolean sendMax) {
super(getDummyAddress(), label, amount, sendMax);
this(silentPaymentAddress, getDummyAddress(), label, amount, sendMax);
}
public SilentPayment(SilentPaymentAddress silentPaymentAddress, Address address, String label, long amount, boolean sendMax) {
super(address == null ? getDummyAddress() : address, label, amount, sendMax, Type.DEFAULT);
this.silentPaymentAddress = silentPaymentAddress;
}
public static Address getDummyAddress() {
return new P2TRAddress(Utils.hexToBytes("0000000000000000000000000000000000000000000000000000000000000000"));
return new P2TRAddress(new byte[32]);
}
public boolean isAddressComputed() {
return !getAddress().equals(getDummyAddress());
}
public SilentPaymentAddress getSilentPaymentAddress() {
return silentPaymentAddress;
}
@Override
public String getDisplayAddress() {
if(!isAddressComputed()) {
return silentPaymentAddress.toAbbreviatedString();
}
return super.getDisplayAddress();
}
}

View file

@ -62,6 +62,11 @@ public class SilentPaymentAddress {
return getAddress();
}
public String toAbbreviatedString() {
String address = toString();
return address.substring(0, 50) + "...";
}
@Override
public final boolean equals(Object o) {
if(this == o) {

View file

@ -3,7 +3,6 @@ package com.sparrowwallet.drongo.silentpayments;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.MnemonicException;
import com.sparrowwallet.drongo.wallet.WalletNode;
import org.bitcoin.NativeSecp256k1;
@ -191,7 +190,7 @@ public class SilentPaymentUtils {
return null;
}
public static void updateSilentPayments(List<SilentPayment> silentPayments, Map<BlockTransactionHashIndex, WalletNode> utxos) {
public static void computeOutputAddresses(List<SilentPayment> silentPayments, Map<HashIndex, WalletNode> utxos) throws InvalidSilentPaymentException {
ECKey summedPrivateKey = getSummedPrivateKey(utxos.values());
BigInteger inputHash = getInputHash(utxos.keySet(), summedPrivateKey);
Map<ECKey, List<SilentPayment>> scanKeyGroups = getScanKeyGroups(silentPayments);
@ -203,7 +202,7 @@ public class SilentPaymentUtils {
BigInteger tk = new BigInteger(1, Utils.taggedHash(BIP_0352_SHARED_SECRET_TAG,
Utils.concat(sharedSecret.getPubKey(), ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(k).array())));
if(tk.equals(BigInteger.ZERO) || tk.compareTo(ECKey.CURVE.getCurve().getOrder()) >= 0) {
throw new IllegalArgumentException("The tk value is invalid for the eligible silent payments inputs");
throw new InvalidSilentPaymentException("The tk value is invalid for the eligible silent payments inputs");
}
ECKey spendKey = silentPayment.getSilentPaymentAddress().getSpendKey();
ECKey pkm = spendKey.add(ECKey.fromPublicOnly(ECKey.publicPointFromPrivate(tk).getEncoded(true)), true);
@ -224,18 +223,18 @@ public class SilentPaymentUtils {
return scanKeyGroups;
}
public static BigInteger getInputHash(Set<BlockTransactionHashIndex> outpoints, ECKey summedPrivateKey) {
public static BigInteger getInputHash(Set<HashIndex> outpoints, ECKey summedPrivateKey) throws InvalidSilentPaymentException {
byte[] smallestOutpoint = getSmallestOutpoint(outpoints);
byte[] concat = Utils.concat(smallestOutpoint, summedPrivateKey.getPubKey());
BigInteger inputHash = new BigInteger(1, Utils.taggedHash(BIP_0352_INPUTS_TAG, concat));
if(inputHash.equals(BigInteger.ZERO) || inputHash.compareTo(ECKey.CURVE.getCurve().getOrder()) >= 0) {
throw new IllegalArgumentException("The input hash is invalid for the eligible silent payments inputs");
throw new InvalidSilentPaymentException("The input hash is invalid for the eligible silent payments inputs");
}
return inputHash;
}
public static ECKey getSummedPrivateKey(Collection<WalletNode> walletNodes) {
public static ECKey getSummedPrivateKey(Collection<WalletNode> walletNodes) throws InvalidSilentPaymentException {
ECKey summedPrivateKey = null;
for(WalletNode walletNode : walletNodes) {
if(!walletNode.getWallet().canSendSilentPayments()) {
@ -243,8 +242,8 @@ public class SilentPaymentUtils {
}
try {
ECKey privateKey = walletNode.getWallet().getKeystores().getFirst().getKey(walletNode);
if(walletNode.getWallet().getScriptType() == P2TR && !privateKey.getPubKeyPoint().getYCoord().toBigInteger().mod(BigInteger.TWO).equals(BigInteger.ZERO)) {
ECKey privateKey = walletNode.getWallet().getScriptType().getOutputKey(walletNode.getWallet().getKeystores().getFirst().getKey(walletNode));
if(walletNode.getWallet().getScriptType() == P2TR && !privateKey.getPubKeyPoint().normalize().getYCoord().toBigInteger().mod(BigInteger.TWO).equals(BigInteger.ZERO)) {
privateKey = privateKey.negatePrivate();
}
if(summedPrivateKey == null) {
@ -253,22 +252,22 @@ public class SilentPaymentUtils {
summedPrivateKey = summedPrivateKey.addPrivate(privateKey);
}
} catch(MnemonicException e) {
throw new IllegalArgumentException("Invalid wallet mnemonic for sending silent payment", e);
throw new InvalidSilentPaymentException("Invalid wallet mnemonic for sending silent payment", e);
}
}
if(summedPrivateKey == null) {
throw new IllegalArgumentException("There are no eligible inputs to derive a silent payments shared secret");
throw new InvalidSilentPaymentException("There are no eligible inputs to derive a silent payments shared secret");
}
if(summedPrivateKey.getPrivKey().equals(BigInteger.ZERO)) {
throw new IllegalArgumentException("The summed private key is zero for the eligible silent payments inputs");
throw new InvalidSilentPaymentException("The summed private key is zero for the eligible silent payments inputs");
}
return summedPrivateKey;
}
public static byte[] getSmallestOutpoint(Set<BlockTransactionHashIndex> outpoints) {
public static byte[] getSmallestOutpoint(Set<HashIndex> outpoints) {
return outpoints.stream().map(outpoint -> new TransactionOutPoint(outpoint.getHash(), outpoint.getIndex())).map(TransactionOutPoint::bitcoinSerialize)
.min(new Utils.LexicographicByteArrayComparator()).orElseThrow(() -> new IllegalArgumentException("No inputs provided to calculate silent payments input hash"));
}

View file

@ -2,6 +2,8 @@ package com.sparrowwallet.drongo.uri;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.wallet.Payment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -66,6 +68,7 @@ public class BitcoinURI {
public static final String FIELD_PAYMENT_REQUEST_URL = "r";
public static final String FIELD_PAYJOIN_URL = "pj";
public static final String FIELD_PAYJOIN_OUTPUT_SUBSTITUTION = "pjos";
public static final String FIELD_SILENT_PAYMENTS_ADDRESS = "sp";
public static final String BITCOIN_SCHEME = "bitcoin";
private static final String ENCODED_SPACE_CHARACTER = "%20";
@ -291,6 +294,22 @@ public class BitcoinURI {
return !"0".equals(parameterMap.get(FIELD_PAYJOIN_OUTPUT_SUBSTITUTION));
}
/**
* @return The silent payments address in the URI, if provided
*/
public final SilentPaymentAddress getSilentPaymentAddress() {
String address = (String)parameterMap.get(FIELD_SILENT_PAYMENTS_ADDRESS);
if(address != null) {
try {
return SilentPaymentAddress.from(address);
} catch(Exception e) {
log.error("Invalid silent payments address provided", e);
}
}
return null;
}
/**
* @param name The name of the parameter
* @return The parameter value, or null if not present
@ -321,6 +340,10 @@ public class BitcoinURI {
public Payment toPayment() {
long amount = getAmount() == null ? -1 : getAmount();
SilentPaymentAddress silentPaymentAddress = getSilentPaymentAddress();
if(getAddress() == null && silentPaymentAddress != null) {
return new SilentPayment(silentPaymentAddress, getLabel(), amount, false);
}
return new Payment(getAddress(), getLabel(), amount, false);
}

View file

@ -2,6 +2,8 @@ package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.P2AAddress;
import com.sparrowwallet.drongo.dns.DnsPayment;
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
public class Payment {
private Address address;
@ -65,4 +67,18 @@ public class Payment {
public enum Type {
DEFAULT, WHIRLPOOL_FEE, FAKE_MIX, MIX, ANCHOR;
}
public String getDisplayAddress() {
return address.toString();
}
@Override
public String toString() {
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(this);
if(dnsPayment != null) {
return dnsPayment.toString();
}
return getDisplayAddress();
}
}

View file

@ -13,8 +13,7 @@ import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.psbt.PSBTOutput;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentUtils;
import com.sparrowwallet.drongo.silentpayments.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
@ -44,6 +43,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
private final Map<String, String> detachedLabels = new HashMap<>();
private WalletConfig walletConfig;
private final Map<TableType, WalletTable> walletTables = new HashMap<>();
private final Map<Address, SilentPaymentAddress> silentPaymentAddresses = new HashMap<>();
private MixConfig mixConfig;
private final Map<Sha256Hash, UtxoMixData> utxoMixes = new HashMap<>();
private Integer storedBlockHeight;
@ -482,6 +482,31 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
return walletTables.get(tableType);
}
public SilentPaymentAddress getSilentPaymentAddress(Address address) {
return silentPaymentAddresses.get(address);
}
private void addSilentPaymentAddress(Address address, SilentPaymentAddress silentPaymentAddress) {
silentPaymentAddresses.put(address, silentPaymentAddress);
}
public void clearSilentPaymentAddress(Address address) {
silentPaymentAddresses.remove(address);
}
public boolean isSilentPaymentsTransaction(BlockTransaction blockTransaction) {
Wallet wallet = isNested() ? getMasterWallet() : this;
return blockTransaction.getTransaction().getOutputs().stream().map(output -> output.getScript().getToAddress())
.filter(Objects::nonNull).anyMatch(address -> wallet.getSilentPaymentAddress(address) != null);
}
public boolean isSafeToAddInputsOrOutputs(BlockTransaction blockTransaction) {
//Silent payments transactions should always signal RBF disabled as adding inputs or other silent payments outputs in the replacement tx can ~burn coins
//If we are ignoring the RBF flag due to mempoolfullrbf, determine if it's safe to add inputs or outputs
return blockTransaction.getTransaction().isReplaceByFee() || isSilentPaymentsTransaction(blockTransaction) || !canSendSilentPayments()
|| !SilentPaymentUtils.containsTaprootOutput(blockTransaction.getTransaction());
}
public MixConfig getMixConfig() {
return mixConfig;
}
@ -1011,7 +1036,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
public WalletTransaction createWalletTransaction(List<UtxoSelector> utxoSelectors, List<TxoFilter> txoFilters, List<Payment> payments, List<byte[]> opReturns,
Set<WalletNode> excludedChangeNodes, double feeRate, double longTermFeeRate, double minRelayFeeRate, Long fee,
Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs) throws InsufficientFundsException {
Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs, boolean allowRbf) throws InsufficientFundsException {
boolean sendMax = payments.stream().anyMatch(Payment::isSendMax);
long totalPaymentAmount = payments.stream().map(Payment::getAmount).mapToLong(v -> v).sum();
Map<BlockTransactionHashIndex, WalletNode> availableTxos = getWalletTxos(txoFilters);
@ -1040,7 +1065,9 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
long totalSelectedAmt = selectedUtxos.keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
int numSets = selectedUtxoSets.size();
List<Payment> txPayments = new ArrayList<>(payments);
List<WalletTransaction.Output> outputs = new ArrayList<>();
Set<WalletNode> txExcludedChangeNodes = new HashSet<>(excludedChangeNodes);
long sequence = allowRbf ? TransactionInput.SEQUENCE_RBF_ENABLED : TransactionInput.SEQUENCE_RBF_DISABLED;
Transaction transaction = new Transaction();
transaction.setVersion(2);
@ -1053,12 +1080,10 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
Transaction prevTx = getWalletTransaction(selectedUtxo.getKey().getHash()).getTransaction();
TransactionOutput prevTxOut = prevTx.getOutputs().get((int)selectedUtxo.getKey().getIndex());
TransactionInput txInput = addDummySpendingInput(transaction, selectedUtxo.getValue(), prevTxOut);
//Enable opt-in RBF by default, matching Bitcoin Core and Electrum
txInput.setSequenceNumber(TransactionInput.SEQUENCE_RBF_ENABLED);
txInput.setSequenceNumber(sequence);
}
if(getScriptType() == P2TR && currentBlockHeight != null) {
if(getScriptType() == P2TR && currentBlockHeight != null && sequence != TransactionInput.SEQUENCE_RBF_DISABLED) {
applySequenceAntiFeeSniping(transaction, selectedUtxos, currentBlockHeight);
}
@ -1070,20 +1095,21 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
txPayments.add(fakeMixPayment);
}
List<SilentPayment> silentPayments = payments.stream().filter(payment -> payment instanceof SilentPayment)
.map(payment -> (SilentPayment)payment).collect(Collectors.toList());
if(!silentPayments.isEmpty()) {
SilentPaymentUtils.updateSilentPayments(silentPayments, selectedUtxos);
}
//Add recipient outputs
for(Payment payment : txPayments) {
transaction.addOutput(payment.getAmount(), payment.getAddress());
if(payment instanceof SilentPayment silentPayment) {
TransactionOutput output = transaction.addOutput(payment.getAmount(), new Script(new byte[0]));
outputs.add(new WalletTransaction.SilentPaymentOutput(output, silentPayment));
} else {
TransactionOutput output = transaction.addOutput(payment.getAmount(), payment.getAddress());
outputs.add(new WalletTransaction.PaymentOutput(output, payment));
}
}
//Add OP_RETURNs
for(byte[] opReturn : opReturns) {
transaction.addOutput(0L, new Script(List.of(ScriptChunk.fromOpcode(ScriptOpCodes.OP_RETURN), ScriptChunk.fromData(opReturn))));
TransactionOutput output = transaction.addOutput(0L, new Script(List.of(ScriptChunk.fromOpcode(ScriptOpCodes.OP_RETURN), ScriptChunk.fromData(opReturn))));
outputs.add(new WalletTransaction.NonAddressOutput(output));
}
double noChangeVSize = transaction.getVirtualSize();
@ -1142,7 +1168,8 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
Map<WalletNode, Long> changeMap = new LinkedHashMap<>();
setChangeAmts = getSetChangeAmounts(selectedUtxoSets, totalPaymentAmount, changeFeeRequiredAmt);
for(Long setChangeAmt : setChangeAmts) {
transaction.addOutput(setChangeAmt, changeNode.getOutputScript());
TransactionOutput output = transaction.addOutput(setChangeAmt, changeNode.getOutputScript());
outputs.add(new WalletTransaction.ChangeOutput(output, changeNode, setChangeAmt));
changeMap.put(changeNode, setChangeAmt);
changeNode = getFreshNode(getChangeKeyPurpose(), changeNode);
}
@ -1151,7 +1178,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
//The new fee has meant that one of the change outputs is now dust. We pay too high a fee without change, but change is dust when added.
if(numSets > 1 && differenceAmt / transaction.getVirtualSize() < noChangeFeeRate * 2) {
//Maximize privacy. Pay a higher fee to keep multiple output sets.
return new WalletTransaction(this, transaction, utxoSelectors, selectedUtxoSets, txPayments, differenceAmt);
return new WalletTransaction(this, transaction, utxoSelectors, selectedUtxoSets, txPayments, outputs, differenceAmt);
} else {
//Maxmize efficiency. Increase value required from inputs and try again.
valueRequiredAmt = totalSelectedAmt + 1;
@ -1159,10 +1186,10 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
}
}
return new WalletTransaction(this, transaction, utxoSelectors, selectedUtxoSets, txPayments, changeMap, changeFeeRequiredAmt);
return new WalletTransaction(this, transaction, utxoSelectors, selectedUtxoSets, txPayments, outputs, changeMap, changeFeeRequiredAmt);
}
return new WalletTransaction(this, transaction, utxoSelectors, selectedUtxoSets, txPayments, differenceAmt);
return new WalletTransaction(this, transaction, utxoSelectors, selectedUtxoSets, txPayments, outputs, differenceAmt);
}
}
@ -1601,7 +1628,10 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
}
public void sign(PSBT psbt) throws MnemonicException {
Map<PSBTInput, WalletNode> signingNodes = getSigningNodes(psbt);
sign(getSigningNodes(psbt));
}
public void sign(Map<PSBTInput, WalletNode> signingNodes) throws MnemonicException {
for(Map.Entry<PSBTInput, WalletNode> signingEntry : signingNodes.entrySet()) {
Wallet signingWallet = signingEntry.getValue().getWallet();
for(Keystore keystore : signingWallet.getKeystores()) {
@ -1617,6 +1647,37 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
}
}
public List<SilentPayment> computeSilentPaymentOutputs(PSBT psbt, Map<PSBTInput, WalletNode> signingNodes) throws InvalidSilentPaymentException {
List<PSBTOutput> silentOutputs = psbt.getPsbtOutputs().stream().filter(psbtOutput -> psbtOutput.getSilentPaymentAddress() != null).collect(Collectors.toList());
if(!silentOutputs.isEmpty()) {
if(psbt.getPsbtInputs().size() != signingNodes.size()) {
throw new InvalidSilentPaymentException("All inputs must be from wallet to calculate silent payment addresses");
}
List<SilentPayment> silentPayments = silentOutputs.stream()
.map(psbtOutput -> new SilentPayment(psbtOutput.getSilentPaymentAddress(), null, psbtOutput.getAmount(), false)).collect(Collectors.toList());
Map<HashIndex, WalletNode> utxos = signingNodes.keySet().stream()
.collect(Collectors.toMap(psbtInput -> new HashIndex(psbtInput.getPrevTxid(), psbtInput.getPrevIndex()), signingNodes::get));
SilentPaymentUtils.computeOutputAddresses(silentPayments, utxos);
for(int i = 0; i < silentOutputs.size(); i++) {
PSBTOutput silentOutput = silentOutputs.get(i);
SilentPayment silentPayment = silentPayments.get(i);
if(!silentPayment.isAddressComputed()) {
throw new InvalidSilentPaymentException("Silent payment address was not calculated");
}
Script outputScript = silentPayment.getAddress().getOutputScript();
silentOutput.setScript(outputScript);
silentOutput.getOutput().setScriptBytes(outputScript.getProgram());
addSilentPaymentAddress(silentPayment.getAddress(), silentPayment.getSilentPaymentAddress());
}
return silentPayments;
}
return Collections.emptyList();
}
public void finalise(PSBT psbt) {
int threshold = getDefaultPolicy().getNumSignaturesRequired();
Map<PSBTInput, WalletNode> signingNodes = getSigningNodes(psbt);

View file

@ -5,8 +5,10 @@ import com.sparrowwallet.drongo.dns.DnsPayment;
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
import java.util.*;
import java.util.stream.Collectors;
/**
* WalletTransaction contains a draft transaction along with associated metadata. The draft transaction has empty signatures but is otherwise complete.
@ -24,17 +26,20 @@ public class WalletTransaction {
private final List<Output> outputs;
private Map<Wallet, Map<Address, WalletNode>> addressNodeMap = new HashMap<>();
private Map<Address, Map<String, byte[]>> addressDnsProofMap = new HashMap<>();
public WalletTransaction(Wallet wallet, Transaction transaction, List<UtxoSelector> utxoSelectors, List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets, List<Payment> payments, long fee) {
this(wallet, transaction, utxoSelectors, selectedUtxoSets, payments, Collections.emptyMap(), fee);
public WalletTransaction(Wallet wallet, Transaction transaction, List<UtxoSelector> utxoSelectors, List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets, List<Payment> payments, List<Output> outputs, long fee) {
this(wallet, transaction, utxoSelectors, selectedUtxoSets, payments, outputs, Collections.emptyMap(), fee);
}
public WalletTransaction(Wallet wallet, Transaction transaction, List<UtxoSelector> utxoSelectors, List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets, List<Payment> payments, Map<WalletNode, Long> changeMap, long fee) {
this(wallet, transaction, utxoSelectors, selectedUtxoSets, payments, changeMap, fee, Collections.emptyMap());
public WalletTransaction(Wallet wallet, Transaction transaction, List<UtxoSelector> utxoSelectors, List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets, List<Payment> payments, List<Output> outputs, Map<WalletNode, Long> changeMap, long fee) {
this(wallet, transaction, utxoSelectors, selectedUtxoSets, payments, outputs, changeMap, fee, Collections.emptyMap());
}
public WalletTransaction(Wallet wallet, Transaction transaction, List<UtxoSelector> utxoSelectors, List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets, List<Payment> payments, Map<WalletNode, Long> changeMap, long fee, Map<Sha256Hash, BlockTransaction> inputTransactions) {
public WalletTransaction(Wallet wallet, Transaction transaction, List<UtxoSelector> utxoSelectors, List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets, List<Payment> payments, List<Output> outputs, Map<WalletNode, Long> changeMap, long fee, Map<Sha256Hash, BlockTransaction> inputTransactions) {
if(!outputs.stream().map(Output::getTransactionOutput).collect(Collectors.toSet()).containsAll(transaction.getOutputs())) {
throw new IllegalArgumentException("Transaction output list does not contain all outputs from the transaction");
}
this.wallet = wallet;
this.transaction = transaction;
this.utxoSelectors = utxoSelectors;
@ -43,7 +48,7 @@ public class WalletTransaction {
this.changeMap = changeMap;
this.fee = fee;
this.inputTransactions = inputTransactions;
this.outputs = calculateOutputs();
this.outputs = outputs;
for(Payment payment : payments) {
payment.setLabel(getOutputLabel(payment));
@ -113,7 +118,7 @@ public class WalletTransaction {
}
public List<Output> getOutputs() {
return outputs;
return Collections.unmodifiableList(outputs);
}
/**
@ -203,7 +208,8 @@ public class WalletTransaction {
}
public boolean isDuplicateAddress(Payment payment) {
return getPayments().stream().filter(p -> payment != p).anyMatch(p -> payment.getAddress() != null && payment.getAddress().equals(p.getAddress()));
return getPayments().stream().filter(p -> payment != p && !(payment instanceof SilentPayment))
.anyMatch(p -> payment.getAddress() != null && payment.getAddress().equals(p.getAddress()));
}
public void updateAddressNodeMap(Map<Wallet, Map<Address, WalletNode>> addressNodeMap, Wallet wallet) {
@ -237,46 +243,6 @@ public class WalletTransaction {
return walletAddressNodeMap;
}
public Map<Address, Map<String, byte[]>> getAddressDnsProofMap() {
for(Payment payment : payments) {
if(addressDnsProofMap.containsKey(payment.getAddress())) {
continue;
}
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment.getAddress());
if(dnsPayment != null && dnsPayment.bitcoinURI().getAddress() != null) {
addressDnsProofMap.put(dnsPayment.bitcoinURI().getAddress(), Map.of(dnsPayment.hrn(), dnsPayment.proofChain()));
}
}
return addressDnsProofMap;
}
private List<Output> calculateOutputs() {
List<Output> outputs = new ArrayList<>();
for(int i = 0, paymentIndex = 0; i < transaction.getOutputs().size(); i++) {
TransactionOutput txOutput = transaction.getOutputs().get(i);
Address address = txOutput.getScript().getToAddress();
if(address == null) {
outputs.add(new NonAddressOutput(txOutput));
} else if(paymentIndex < payments.size()) {
Payment payment = payments.get(paymentIndex++);
outputs.add(new PaymentOutput(txOutput, payment));
}
}
Set<Integer> seenIndexes = new HashSet<>();
for(Map.Entry<WalletNode, Long> changeEntry : changeMap.entrySet()) {
int outputIndex = getOutputIndex(changeEntry.getKey().getAddress(), changeEntry.getValue(), seenIndexes);
TransactionOutput txOutput = transaction.getOutputs().get(outputIndex);
seenIndexes.add(outputIndex);
outputs.add(outputIndex, new ChangeOutput(txOutput, changeEntry.getKey(), changeEntry.getValue()));
}
return outputs;
}
public static class Output {
private final TransactionOutput transactionOutput;
@ -287,6 +253,10 @@ public class WalletTransaction {
public TransactionOutput getTransactionOutput() {
return transactionOutput;
}
public Map<String, byte[]> getDnsSecProof() {
return null;
}
}
public static class NonAddressOutput extends Output {
@ -306,6 +276,29 @@ public class WalletTransaction {
public Payment getPayment() {
return payment;
}
public Map<String, byte[]> getDnsSecProof() {
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
if(dnsPayment != null) {
if(dnsPayment.hasAddress()) {
return Map.of(dnsPayment.hrn(), dnsPayment.proofChain());
} else if(dnsPayment.hasSilentPaymentAddress()) {
return Map.of(dnsPayment.hrn(), dnsPayment.proofChain());
}
}
return super.getDnsSecProof();
}
}
public static class SilentPaymentOutput extends PaymentOutput {
public SilentPaymentOutput(TransactionOutput transactionOutput, SilentPayment silentPayment) {
super(transactionOutput, silentPayment);
}
public SilentPayment getSilentPayment() {
return (SilentPayment)getPayment();
}
}
public static class ChangeOutput extends Output {

View file

@ -1,17 +1,17 @@
package com.sparrowwallet.drongo.silentpayments;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.P2TRAddress;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.drongo.wallet.*;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.math.BigInteger;
import java.util.*;
public class SilentPaymentUtilsTest {
@ -75,6 +75,58 @@ public class SilentPaymentUtilsTest {
Assertions.assertEquals("0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", Utils.bytesToHex(tweak));
}
@Test
public void testTweakTaprootEvenY() {
Transaction transaction = new Transaction();
transaction.addInput(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0, new Script(new byte[0]), new TransactionWitness(transaction, List.of(Utils.hexToBytes("0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b"))));
transaction.addInput(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0, new Script(new byte[0]), new TransactionWitness(transaction, List.of(Utils.hexToBytes("0140bd1e708f92dbeaf24a6b8dd22e59c6274355424d62baea976b449e220fd75b13578e262ab11b7aa58e037f0c6b0519b66803b7d9decaa1906dedebfb531c56c1"))));
transaction.addOutput(1000L, ScriptType.P2TR.getOutputScript(ECKey.fromPublicOnly(Utils.hexToBytes("de88bea8e7ffc9ce1af30d1132f910323c505185aec8eae361670421e749a1fb"))));
Map<HashIndex, Script> spentScriptPubKeys = new HashMap<>();
HashIndex hashIndex0 = new HashIndex(transaction.getInputs().getFirst().getOutpoint().getHash(), transaction.getInputs().getFirst().getOutpoint().getIndex());
Script spentScriptPubKey0 = new Script(Utils.hexToBytes("51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5"));
HashIndex hashIndex1 = new HashIndex(transaction.getInputs().getLast().getOutpoint().getHash(), transaction.getInputs().getLast().getOutpoint().getIndex());
Script spentScriptPubKey1 = new Script(Utils.hexToBytes("5120782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338"));
spentScriptPubKeys.put(hashIndex0, spentScriptPubKey0);
spentScriptPubKeys.put(hashIndex1, spentScriptPubKey1);
byte[] tweak = SilentPaymentUtils.getTweak(transaction, spentScriptPubKeys);
Assertions.assertNotNull(tweak);
Assertions.assertEquals("02dc59cc8e8873b65c1dd5c416d4fbeb647372c329bd84a70c05b310e222e2c183", Utils.bytesToHex(tweak));
}
@Test
public void testTweakTaprootMixedY() {
Transaction transaction = new Transaction();
transaction.addInput(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0, new Script(new byte[0]), new TransactionWitness(transaction, List.of(Utils.hexToBytes("0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b"))));
transaction.addInput(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0, new Script(new byte[0]), new TransactionWitness(transaction, List.of(Utils.hexToBytes("01400a4d0dca6293f40499394d7eefe14a1de11e0e3454f51de2e802592abf5ee549042a1b1a8fb2e149ee9dd3f086c1b69b2f182565ab6ecf599b1ec9ebadfda6c5"))));
transaction.addOutput(1000L, new P2TRAddress(Utils.hexToBytes("77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1")).getOutputScript());
Map<HashIndex, Script> spentScriptPubKeys = new HashMap<>();
HashIndex hashIndex0 = new HashIndex(transaction.getInputs().getFirst().getOutpoint().getHash(), transaction.getInputs().getFirst().getOutpoint().getIndex());
Script spentScriptPubKey0 = new Script(Utils.hexToBytes("51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5"));
HashIndex hashIndex1 = new HashIndex(transaction.getInputs().getLast().getOutpoint().getHash(), transaction.getInputs().getLast().getOutpoint().getIndex());
Script spentScriptPubKey1 = new Script(Utils.hexToBytes("51208c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4"));
spentScriptPubKeys.put(hashIndex0, spentScriptPubKey0);
spentScriptPubKeys.put(hashIndex1, spentScriptPubKey1);
byte[] tweak = SilentPaymentUtils.getTweak(transaction, spentScriptPubKeys);
Assertions.assertNotNull(tweak);
Assertions.assertEquals("03b990f5b1d90ea8fd4bdd5c856a9dfe17035d196958062e2c6cb4c99e413f3548", Utils.bytesToHex(tweak));
ECKey tweakKey = ECKey.fromPublicOnly(tweak);
BigInteger scanPrivateKey = new BigInteger(1, Utils.hexToBytes("0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c"));
ECKey sharedSecret = tweakKey.multiply(scanPrivateKey, true);
Assertions.assertEquals("030e7f5ca4bf109fc35c8c2d878f756c891ac04c456cc5f0b05fcec4d3b2b1beb2", Utils.bytesToHex(sharedSecret.getPubKey()));
byte[] tk = Utils.taggedHash(SilentPaymentUtils.BIP_0352_SHARED_SECRET_TAG, Utils.concat(sharedSecret.getPubKey(true), new byte[4]));
ECKey tkKey = ECKey.fromPrivate(tk);
ECKey bSpend = ECKey.fromPublicOnly(ECKey.fromPrivate(Utils.hexToBytes("9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3")));
ECKey Pk = bSpend.add(tkKey, true);
Assertions.assertEquals("77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1", Utils.bytesToHex(Pk.getPubKeyXCoord()));
Address address = new P2TRAddress(Pk.getPubKeyXCoord());
Assertions.assertEquals(transaction.getOutputs().getFirst().getScript().getToAddress(), address);
}
@Test
public void testInvalidOutput() {
Transaction transaction = new Transaction();
@ -86,21 +138,48 @@ public class SilentPaymentUtilsTest {
}
@Test
public void testSimpleSendTwoInputs() {
public void testSimpleSendTwoInputs() throws InvalidSilentPaymentException {
Transaction transaction = new Transaction();
transaction.addInput(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0, new Script(Utils.hexToBytes("483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5")));
transaction.addInput(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0, new Script(Utils.hexToBytes("48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792")));
transaction.addOutput(1000L, ScriptType.P2TR.getOutputScript(ECKey.fromPublicOnly(Utils.hexToBytes("3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1"))));
Map<HashIndex, Script> spentScriptPubKeys = new HashMap<>();
HashIndex hashIndex0 = new HashIndex(transaction.getInputs().getFirst().getOutpoint().getHash(), transaction.getInputs().getFirst().getOutpoint().getIndex());
Script spentScriptPubKey0 = new Script(Utils.hexToBytes("76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac"));
HashIndex hashIndex1 = new HashIndex(transaction.getInputs().getLast().getOutpoint().getHash(), transaction.getInputs().getLast().getOutpoint().getIndex());
Script spentScriptPubKey1 = new Script(Utils.hexToBytes("76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac"));
spentScriptPubKeys.put(hashIndex0, spentScriptPubKey0);
spentScriptPubKeys.put(hashIndex1, spentScriptPubKey1);
byte[] tweak = SilentPaymentUtils.getTweak(transaction, spentScriptPubKeys);
Assertions.assertNotNull(tweak);
Assertions.assertEquals("024ac253c216532e961988e2a8ce266a447c894c781e52ef6cee902361db960004", Utils.bytesToHex(tweak));
ECKey tweakKey = ECKey.fromPublicOnly(tweak);
BigInteger scanPrivateKey = new BigInteger(1, Utils.hexToBytes("0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c"));
ECKey sharedSecret = tweakKey.multiply(scanPrivateKey, true);
byte[] tk = Utils.taggedHash(SilentPaymentUtils.BIP_0352_SHARED_SECRET_TAG, Utils.concat(sharedSecret.getPubKey(true), new byte[4]));
ECKey tkKey = ECKey.fromPrivate(tk);
ECKey bSpend = ECKey.fromPublicOnly(ECKey.fromPrivate(Utils.hexToBytes("9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3")));
ECKey Pk = bSpend.add(tkKey, true);
Address address = new P2TRAddress(Pk.getPubKeyXCoord());
Assertions.assertEquals("3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", Utils.bytesToHex(address.getData()));
Wallet sendWallet = new Wallet();
sendWallet.setPolicyType(PolicyType.SINGLE);
sendWallet.setScriptType(ScriptType.P2WPKH);
Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<HashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<WalletNode, ECKey> privateKeys = new LinkedHashMap<>();
WalletNode walletNode0 = new WalletNode(sendWallet, "/0/0");
BlockTransactionHashIndex ref0 = new BlockTransactionHashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0, null, null, 0, 0);
HashIndex ref0 = new HashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0);
ECKey privKey0 = ECKey.fromPrivate(Utils.hexToBytes("eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1"));
utxos.put(ref0, walletNode0);
privateKeys.put(walletNode0, privKey0);
WalletNode walletNode1 = new WalletNode(sendWallet, "/0/1");
BlockTransactionHashIndex ref1 = new BlockTransactionHashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0, null, null, 0, 0);
HashIndex ref1 = new HashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0);
ECKey privKey1 = ECKey.fromPrivate(Utils.hexToBytes("93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16"));
utxos.put(ref1, walletNode1);
privateKeys.put(walletNode1, privKey1);
@ -112,27 +191,60 @@ public class SilentPaymentUtilsTest {
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from("sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv");
List<SilentPayment> silentPayments = List.of(new SilentPayment(silentPaymentAddress, "", 0, false));
SilentPaymentUtils.updateSilentPayments(silentPayments, utxos);
SilentPaymentUtils.computeOutputAddresses(silentPayments, utxos);
Assertions.assertEquals(1, silentPayments.size());
Assertions.assertEquals(silentPayments.getFirst().getAddress(), address);
Assertions.assertEquals("3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", Utils.bytesToHex(silentPayments.getFirst().getAddress().getData()));
}
@Test
public void testSimpleSendTwoInputsReversed() throws InvalidSilentPaymentException {
Wallet sendWallet = new Wallet();
sendWallet.setPolicyType(PolicyType.SINGLE);
sendWallet.setScriptType(ScriptType.P2WPKH);
Map<HashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<WalletNode, ECKey> privateKeys = new LinkedHashMap<>();
WalletNode walletNode0 = new WalletNode(sendWallet, "/0/0");
HashIndex ref0 = new HashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0);
ECKey privKey0 = ECKey.fromPrivate(Utils.hexToBytes("eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1"));
utxos.put(ref0, walletNode0);
privateKeys.put(walletNode0, privKey0);
WalletNode walletNode1 = new WalletNode(sendWallet, "/0/1");
HashIndex ref1 = new HashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0);
ECKey privKey1 = ECKey.fromPrivate(Utils.hexToBytes("93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16"));
utxos.put(ref1, walletNode1);
privateKeys.put(walletNode1, privKey1);
TestKeystore sendKeystore = new TestKeystore(privateKeys);
sendWallet.getKeystores().add(sendKeystore);
sendWallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, sendWallet.getKeystores(), 1));
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from("sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv");
List<SilentPayment> silentPayments = List.of(new SilentPayment(silentPaymentAddress, "", 0, false));
SilentPaymentUtils.computeOutputAddresses(silentPayments, utxos);
Assertions.assertEquals(1, silentPayments.size());
Assertions.assertEquals("3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", Utils.bytesToHex(silentPayments.getFirst().getAddress().getData()));
}
@Test
public void testSimpleSendTwoInputsReversed() {
public void testSimpleSendTwoInputsSameTransaction() throws InvalidSilentPaymentException {
Wallet sendWallet = new Wallet();
sendWallet.setPolicyType(PolicyType.SINGLE);
sendWallet.setScriptType(ScriptType.P2WPKH);
Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<HashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<WalletNode, ECKey> privateKeys = new LinkedHashMap<>();
WalletNode walletNode0 = new WalletNode(sendWallet, "/0/0");
BlockTransactionHashIndex ref0 = new BlockTransactionHashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0, null, null, 0, 0);
HashIndex ref0 = new HashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 3);
ECKey privKey0 = ECKey.fromPrivate(Utils.hexToBytes("eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1"));
utxos.put(ref0, walletNode0);
privateKeys.put(walletNode0, privKey0);
WalletNode walletNode1 = new WalletNode(sendWallet, "/0/1");
BlockTransactionHashIndex ref1 = new BlockTransactionHashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0, null, null, 0, 0);
HashIndex ref1 = new HashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 7);
ECKey privKey1 = ECKey.fromPrivate(Utils.hexToBytes("93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16"));
utxos.put(ref1, walletNode1);
privateKeys.put(walletNode1, privKey1);
@ -144,59 +256,27 @@ public class SilentPaymentUtilsTest {
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from("sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv");
List<SilentPayment> silentPayments = List.of(new SilentPayment(silentPaymentAddress, "", 0, false));
SilentPaymentUtils.updateSilentPayments(silentPayments, utxos);
Assertions.assertEquals(1, silentPayments.size());
Assertions.assertEquals("3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", Utils.bytesToHex(silentPayments.getFirst().getAddress().getData()));
}
@Test
public void testSimpleSendTwoInputsSameTransaction() {
Wallet sendWallet = new Wallet();
sendWallet.setPolicyType(PolicyType.SINGLE);
sendWallet.setScriptType(ScriptType.P2WPKH);
Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<WalletNode, ECKey> privateKeys = new LinkedHashMap<>();
WalletNode walletNode0 = new WalletNode(sendWallet, "/0/0");
BlockTransactionHashIndex ref0 = new BlockTransactionHashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0, null, null, 3, 0);
ECKey privKey0 = ECKey.fromPrivate(Utils.hexToBytes("eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1"));
utxos.put(ref0, walletNode0);
privateKeys.put(walletNode0, privKey0);
WalletNode walletNode1 = new WalletNode(sendWallet, "/0/1");
BlockTransactionHashIndex ref1 = new BlockTransactionHashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0, null, null, 7, 0);
ECKey privKey1 = ECKey.fromPrivate(Utils.hexToBytes("93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16"));
utxos.put(ref1, walletNode1);
privateKeys.put(walletNode1, privKey1);
TestKeystore sendKeystore = new TestKeystore(privateKeys);
sendWallet.getKeystores().add(sendKeystore);
sendWallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, sendWallet.getKeystores(), 1));
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from("sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv");
List<SilentPayment> silentPayments = List.of(new SilentPayment(silentPaymentAddress, "", 0, false));
SilentPaymentUtils.updateSilentPayments(silentPayments, utxos);
SilentPaymentUtils.computeOutputAddresses(silentPayments, utxos);
Assertions.assertEquals(1, silentPayments.size());
Assertions.assertEquals("79e71baa2ba3fc66396de3a04f168c7bf24d6870ec88ca877754790c1db357b6", Utils.bytesToHex(silentPayments.getFirst().getAddress().getData()));
}
@Test
public void testSimpleSendTwoInputsSameTransactionReversed() {
public void testSimpleSendTwoInputsSameTransactionReversed() throws InvalidSilentPaymentException {
Wallet sendWallet = new Wallet();
sendWallet.setPolicyType(PolicyType.SINGLE);
sendWallet.setScriptType(ScriptType.P2WPKH);
Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<HashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<WalletNode, ECKey> privateKeys = new LinkedHashMap<>();
WalletNode walletNode0 = new WalletNode(sendWallet, "/0/0");
BlockTransactionHashIndex ref0 = new BlockTransactionHashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0, null, null, 7, 0);
HashIndex ref0 = new HashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 7);
ECKey privKey0 = ECKey.fromPrivate(Utils.hexToBytes("eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1"));
utxos.put(ref0, walletNode0);
privateKeys.put(walletNode0, privKey0);
WalletNode walletNode1 = new WalletNode(sendWallet, "/0/1");
BlockTransactionHashIndex ref1 = new BlockTransactionHashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0, null, null, 3, 0);
HashIndex ref1 = new HashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 3);
ECKey privKey1 = ECKey.fromPrivate(Utils.hexToBytes("93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16"));
utxos.put(ref1, walletNode1);
privateKeys.put(walletNode1, privKey1);
@ -208,27 +288,27 @@ public class SilentPaymentUtilsTest {
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from("sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv");
List<SilentPayment> silentPayments = List.of(new SilentPayment(silentPaymentAddress, "", 0, false));
SilentPaymentUtils.updateSilentPayments(silentPayments, utxos);
SilentPaymentUtils.computeOutputAddresses(silentPayments, utxos);
Assertions.assertEquals(1, silentPayments.size());
Assertions.assertEquals("f4c2da807f89cb1501f1a77322a895acfb93c28e08ed2724d2beb8e44539ba38", Utils.bytesToHex(silentPayments.getFirst().getAddress().getData()));
}
@Test
public void testOutpointOrderingIndex() {
public void testOutpointOrderingIndex() throws InvalidSilentPaymentException {
Wallet sendWallet = new Wallet();
sendWallet.setPolicyType(PolicyType.SINGLE);
sendWallet.setScriptType(ScriptType.P2WPKH);
Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<HashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<WalletNode, ECKey> privateKeys = new LinkedHashMap<>();
WalletNode walletNode0 = new WalletNode(sendWallet, "/0/0");
BlockTransactionHashIndex ref0 = new BlockTransactionHashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0, null, null, 1, 0);
HashIndex ref0 = new HashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 1);
ECKey privKey0 = ECKey.fromPrivate(Utils.hexToBytes("eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1"));
utxos.put(ref0, walletNode0);
privateKeys.put(walletNode0, privKey0);
WalletNode walletNode1 = new WalletNode(sendWallet, "/0/1");
BlockTransactionHashIndex ref1 = new BlockTransactionHashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0, null, null, 256, 0);
HashIndex ref1 = new HashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 256);
ECKey privKey1 = ECKey.fromPrivate(Utils.hexToBytes("93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16"));
utxos.put(ref1, walletNode1);
privateKeys.put(walletNode1, privKey1);
@ -240,27 +320,27 @@ public class SilentPaymentUtilsTest {
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from("sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv");
List<SilentPayment> silentPayments = List.of(new SilentPayment(silentPaymentAddress, "", 0, false));
SilentPaymentUtils.updateSilentPayments(silentPayments, utxos);
SilentPaymentUtils.computeOutputAddresses(silentPayments, utxos);
Assertions.assertEquals(1, silentPayments.size());
Assertions.assertEquals("a85ef8701394b517a4b35217c4bd37ac01ebeed4b008f8d0879f9e09ba95319c", Utils.bytesToHex(silentPayments.getFirst().getAddress().getData()));
}
@Test
public void testSingleRecipientSamePubKey() {
public void testSingleRecipientSamePubKey() throws InvalidSilentPaymentException {
Wallet sendWallet = new Wallet();
sendWallet.setPolicyType(PolicyType.SINGLE);
sendWallet.setScriptType(ScriptType.P2WPKH);
Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<HashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<WalletNode, ECKey> privateKeys = new LinkedHashMap<>();
WalletNode walletNode0 = new WalletNode(sendWallet, "/0/0");
BlockTransactionHashIndex ref0 = new BlockTransactionHashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0, null, null, 0, 0);
HashIndex ref0 = new HashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0);
ECKey privKey0 = ECKey.fromPrivate(Utils.hexToBytes("eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1"));
utxos.put(ref0, walletNode0);
privateKeys.put(walletNode0, privKey0);
WalletNode walletNode1 = new WalletNode(sendWallet, "/0/1");
BlockTransactionHashIndex ref1 = new BlockTransactionHashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0, null, null, 0, 0);
HashIndex ref1 = new HashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0);
ECKey privKey1 = ECKey.fromPrivate(Utils.hexToBytes("eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1"));
utxos.put(ref1, walletNode1);
privateKeys.put(walletNode1, privKey1);
@ -272,28 +352,38 @@ public class SilentPaymentUtilsTest {
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from("sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv");
List<SilentPayment> silentPayments = List.of(new SilentPayment(silentPaymentAddress, "", 0, false));
SilentPaymentUtils.updateSilentPayments(silentPayments, utxos);
SilentPaymentUtils.computeOutputAddresses(silentPayments, utxos);
Assertions.assertEquals(1, silentPayments.size());
Assertions.assertEquals("548ae55c8eec1e736e8d3e520f011f1f42a56d166116ad210b3937599f87f566", Utils.bytesToHex(silentPayments.getFirst().getAddress().getData()));
}
@Test
public void testSingleRecipientTaprootOnlyEvenY() {
public void testSingleRecipientTaprootOnlyEvenY() throws InvalidSilentPaymentException {
Wallet sendWallet = new Wallet();
sendWallet.setPolicyType(PolicyType.SINGLE);
sendWallet.setScriptType(ScriptType.P2TR);
Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<HashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<WalletNode, ECKey> privateKeys = new LinkedHashMap<>();
WalletNode walletNode0 = new WalletNode(sendWallet, "/0/0");
BlockTransactionHashIndex ref0 = new BlockTransactionHashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0, null, null, 0, 0);
ECKey privKey0 = ECKey.fromPrivate(Utils.hexToBytes("eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1"));
HashIndex ref0 = new HashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0);
ECKey privKey0 = new ECKey() {
@Override
public ECKey getTweakedOutputKey() {
return ECKey.fromPrivate(Utils.hexToBytes("eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1"));
}
};
utxos.put(ref0, walletNode0);
privateKeys.put(walletNode0, privKey0);
WalletNode walletNode1 = new WalletNode(sendWallet, "/0/1");
BlockTransactionHashIndex ref1 = new BlockTransactionHashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0, null, null, 0, 0);
ECKey privKey1 = ECKey.fromPrivate(Utils.hexToBytes("fc8716a97a48ba9a05a98ae47b5cd201a25a7fd5d8b73c203c5f7b6b6b3b6ad7"));
HashIndex ref1 = new HashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0);
ECKey privKey1 = new ECKey() {
@Override
public ECKey getTweakedOutputKey() {
return ECKey.fromPrivate(Utils.hexToBytes("fc8716a97a48ba9a05a98ae47b5cd201a25a7fd5d8b73c203c5f7b6b6b3b6ad7"));
}
};
utxos.put(ref1, walletNode1);
privateKeys.put(walletNode1, privKey1);
@ -304,28 +394,38 @@ public class SilentPaymentUtilsTest {
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from("sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv");
List<SilentPayment> silentPayments = List.of(new SilentPayment(silentPaymentAddress, "", 0, false));
SilentPaymentUtils.updateSilentPayments(silentPayments, utxos);
SilentPaymentUtils.computeOutputAddresses(silentPayments, utxos);
Assertions.assertEquals(1, silentPayments.size());
Assertions.assertEquals("de88bea8e7ffc9ce1af30d1132f910323c505185aec8eae361670421e749a1fb", Utils.bytesToHex(silentPayments.getFirst().getAddress().getData()));
}
@Test
public void testSingleRecipientTaprootOnlyMixedY() {
public void testSingleRecipientTaprootOnlyMixedY() throws InvalidSilentPaymentException {
Wallet sendWallet = new Wallet();
sendWallet.setPolicyType(PolicyType.SINGLE);
sendWallet.setScriptType(ScriptType.P2TR);
Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<HashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<WalletNode, ECKey> privateKeys = new LinkedHashMap<>();
WalletNode walletNode0 = new WalletNode(sendWallet, "/0/0");
BlockTransactionHashIndex ref0 = new BlockTransactionHashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0, null, null, 0, 0);
ECKey privKey0 = ECKey.fromPrivate(Utils.hexToBytes("eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1"));
HashIndex ref0 = new HashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0);
ECKey privKey0 = new ECKey() {
@Override
public ECKey getTweakedOutputKey() {
return ECKey.fromPrivate(Utils.hexToBytes("eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1"));
}
};
utxos.put(ref0, walletNode0);
privateKeys.put(walletNode0, privKey0);
WalletNode walletNode1 = new WalletNode(sendWallet, "/0/1");
BlockTransactionHashIndex ref1 = new BlockTransactionHashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0, null, null, 0, 0);
ECKey privKey1 = ECKey.fromPrivate(Utils.hexToBytes("1d37787c2b7116ee983e9f9c13269df29091b391c04db94239e0d2bc2182c3bf"));
HashIndex ref1 = new HashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0);
ECKey privKey1 = new ECKey() {
@Override
public ECKey getTweakedOutputKey() {
return ECKey.fromPrivate(Utils.hexToBytes("1d37787c2b7116ee983e9f9c13269df29091b391c04db94239e0d2bc2182c3bf"));
}
};
utxos.put(ref1, walletNode1);
privateKeys.put(walletNode1, privKey1);
@ -336,13 +436,13 @@ public class SilentPaymentUtilsTest {
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from("sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv");
List<SilentPayment> silentPayments = List.of(new SilentPayment(silentPaymentAddress, "", 0, false));
SilentPaymentUtils.updateSilentPayments(silentPayments, utxos);
SilentPaymentUtils.computeOutputAddresses(silentPayments, utxos);
Assertions.assertEquals(1, silentPayments.size());
Assertions.assertEquals("77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1", Utils.bytesToHex(silentPayments.getFirst().getAddress().getData()));
}
@Test
public void testSingleRecipientTaprootEvenYAndNonTaproot() {
public void testSingleRecipientTaprootEvenYAndNonTaproot() throws InvalidSilentPaymentException {
Wallet taprootWallet = new Wallet();
taprootWallet.setPolicyType(PolicyType.SINGLE);
taprootWallet.setScriptType(ScriptType.P2TR);
@ -353,16 +453,21 @@ public class SilentPaymentUtilsTest {
segwitWallet.setScriptType(ScriptType.P2WPKH);
Map<WalletNode, ECKey> segwitPrivateKeys = new LinkedHashMap<>();
Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<HashIndex, WalletNode> utxos = new LinkedHashMap<>();
WalletNode walletNode0 = new WalletNode(taprootWallet, "/0/0");
BlockTransactionHashIndex ref0 = new BlockTransactionHashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0, null, null, 0, 0);
ECKey privKey0 = ECKey.fromPrivate(Utils.hexToBytes("eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1"));
HashIndex ref0 = new HashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0);
ECKey privKey0 = new ECKey() {
@Override
public ECKey getTweakedOutputKey() {
return ECKey.fromPrivate(Utils.hexToBytes("eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1"));
}
};
utxos.put(ref0, walletNode0);
taprootPrivateKeys.put(walletNode0, privKey0);
WalletNode walletNode1 = new WalletNode(segwitWallet, "/0/1");
BlockTransactionHashIndex ref1 = new BlockTransactionHashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0, null, null, 0, 0);
HashIndex ref1 = new HashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0);
ECKey privKey1 = ECKey.fromPrivate(Utils.hexToBytes("8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3"));
utxos.put(ref1, walletNode1);
segwitPrivateKeys.put(walletNode1, privKey1);
@ -378,13 +483,13 @@ public class SilentPaymentUtilsTest {
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from("sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv");
List<SilentPayment> silentPayments = List.of(new SilentPayment(silentPaymentAddress, "", 0, false));
SilentPaymentUtils.updateSilentPayments(silentPayments, utxos);
SilentPaymentUtils.computeOutputAddresses(silentPayments, utxos);
Assertions.assertEquals(1, silentPayments.size());
Assertions.assertEquals("30523cca96b2a9ae3c98beb5e60f7d190ec5bc79b2d11a0b2d4d09a608c448f0", Utils.bytesToHex(silentPayments.getFirst().getAddress().getData()));
}
@Test
public void testSingleRecipientTaprootOddYAndNonTaproot() {
public void testSingleRecipientTaprootOddYAndNonTaproot() throws InvalidSilentPaymentException {
Wallet taprootWallet = new Wallet();
taprootWallet.setPolicyType(PolicyType.SINGLE);
taprootWallet.setScriptType(ScriptType.P2TR);
@ -395,16 +500,21 @@ public class SilentPaymentUtilsTest {
segwitWallet.setScriptType(ScriptType.P2WPKH);
Map<WalletNode, ECKey> segwitPrivateKeys = new LinkedHashMap<>();
Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<HashIndex, WalletNode> utxos = new LinkedHashMap<>();
WalletNode walletNode0 = new WalletNode(taprootWallet, "/0/0");
BlockTransactionHashIndex ref0 = new BlockTransactionHashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0, null, null, 0, 0);
ECKey privKey0 = ECKey.fromPrivate(Utils.hexToBytes("1d37787c2b7116ee983e9f9c13269df29091b391c04db94239e0d2bc2182c3bf"));
HashIndex ref0 = new HashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0);
ECKey privKey0 = new ECKey() {
@Override
public ECKey getTweakedOutputKey() {
return ECKey.fromPrivate(Utils.hexToBytes("1d37787c2b7116ee983e9f9c13269df29091b391c04db94239e0d2bc2182c3bf"));
}
};
utxos.put(ref0, walletNode0);
taprootPrivateKeys.put(walletNode0, privKey0);
WalletNode walletNode1 = new WalletNode(segwitWallet, "/0/1");
BlockTransactionHashIndex ref1 = new BlockTransactionHashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0, null, null, 0, 0);
HashIndex ref1 = new HashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0);
ECKey privKey1 = ECKey.fromPrivate(Utils.hexToBytes("8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3"));
utxos.put(ref1, walletNode1);
segwitPrivateKeys.put(walletNode1, privKey1);
@ -420,27 +530,27 @@ public class SilentPaymentUtilsTest {
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from("sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv");
List<SilentPayment> silentPayments = List.of(new SilentPayment(silentPaymentAddress, "", 0, false));
SilentPaymentUtils.updateSilentPayments(silentPayments, utxos);
SilentPaymentUtils.computeOutputAddresses(silentPayments, utxos);
Assertions.assertEquals(1, silentPayments.size());
Assertions.assertEquals("359358f59ee9e9eec3f00bdf4882570fd5c182e451aa2650b788544aff012a3a", Utils.bytesToHex(silentPayments.getFirst().getAddress().getData()));
}
@Test
public void testMultipleOutputsSameRecipient() {
public void testMultipleOutputsSameRecipient() throws InvalidSilentPaymentException {
Wallet sendWallet = new Wallet();
sendWallet.setPolicyType(PolicyType.SINGLE);
sendWallet.setScriptType(ScriptType.P2WPKH);
Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<HashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<WalletNode, ECKey> privateKeys = new LinkedHashMap<>();
WalletNode walletNode0 = new WalletNode(sendWallet, "/0/0");
BlockTransactionHashIndex ref0 = new BlockTransactionHashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0, null, null, 0, 0);
HashIndex ref0 = new HashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0);
ECKey privKey0 = ECKey.fromPrivate(Utils.hexToBytes("eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1"));
utxos.put(ref0, walletNode0);
privateKeys.put(walletNode0, privKey0);
WalletNode walletNode1 = new WalletNode(sendWallet, "/0/1");
BlockTransactionHashIndex ref1 = new BlockTransactionHashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0, null, null, 0, 0);
HashIndex ref1 = new HashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0);
ECKey privKey1 = ECKey.fromPrivate(Utils.hexToBytes("0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a"));
utxos.put(ref1, walletNode1);
privateKeys.put(walletNode1, privKey1);
@ -453,7 +563,7 @@ public class SilentPaymentUtilsTest {
SilentPaymentAddress silentPaymentAddress1 = SilentPaymentAddress.from("sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv");
List<SilentPayment> silentPayments = List.of(new SilentPayment(silentPaymentAddress0, "First", 0, false), new SilentPayment(silentPaymentAddress1, "Second", 0, false));
SilentPaymentUtils.updateSilentPayments(silentPayments, utxos);
SilentPaymentUtils.computeOutputAddresses(silentPayments, utxos);
Assertions.assertEquals(2, silentPayments.size());
Assertions.assertEquals("First", silentPayments.getFirst().getLabel());
Assertions.assertEquals("f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", Utils.bytesToHex(silentPayments.getFirst().getAddress().getData()));
@ -462,21 +572,21 @@ public class SilentPaymentUtilsTest {
}
@Test
public void testMultipleOutputsMultipleRecipients() {
public void testMultipleOutputsMultipleRecipients() throws InvalidSilentPaymentException {
Wallet sendWallet = new Wallet();
sendWallet.setPolicyType(PolicyType.SINGLE);
sendWallet.setScriptType(ScriptType.P2WPKH);
Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<HashIndex, WalletNode> utxos = new LinkedHashMap<>();
Map<WalletNode, ECKey> privateKeys = new LinkedHashMap<>();
WalletNode walletNode0 = new WalletNode(sendWallet, "/0/0");
BlockTransactionHashIndex ref0 = new BlockTransactionHashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0, null, null, 0, 0);
HashIndex ref0 = new HashIndex(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0);
ECKey privKey0 = ECKey.fromPrivate(Utils.hexToBytes("eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1"));
utxos.put(ref0, walletNode0);
privateKeys.put(walletNode0, privKey0);
WalletNode walletNode1 = new WalletNode(sendWallet, "/0/1");
BlockTransactionHashIndex ref1 = new BlockTransactionHashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0, null, null, 0, 0);
HashIndex ref1 = new HashIndex(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0);
ECKey privKey1 = ECKey.fromPrivate(Utils.hexToBytes("0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a"));
utxos.put(ref1, walletNode1);
privateKeys.put(walletNode1, privKey1);
@ -490,7 +600,7 @@ public class SilentPaymentUtilsTest {
SilentPaymentAddress silentPaymentAddress2 = SilentPaymentAddress.from("sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn");
List<SilentPayment> silentPayments = List.of(new SilentPayment(silentPaymentAddress0, "First", 0, false), new SilentPayment(silentPaymentAddress1, "Second", 0, false), new SilentPayment(silentPaymentAddress2, "Third", 0, false));
SilentPaymentUtils.updateSilentPayments(silentPayments, utxos);
SilentPaymentUtils.computeOutputAddresses(silentPayments, utxos);
Assertions.assertEquals(3, silentPayments.size());
Assertions.assertEquals("First", silentPayments.getFirst().getLabel());
Assertions.assertEquals("f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", Utils.bytesToHex(silentPayments.getFirst().getAddress().getData()));