diff --git a/src/main/java/com/sparrowwallet/drongo/dns/DnsAddress.java b/src/main/java/com/sparrowwallet/drongo/dns/DnsAddress.java new file mode 100644 index 0000000..427b8bc --- /dev/null +++ b/src/main/java/com/sparrowwallet/drongo/dns/DnsAddress.java @@ -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; + } +} diff --git a/src/main/java/com/sparrowwallet/drongo/dns/DnsPayment.java b/src/main/java/com/sparrowwallet/drongo/dns/DnsPayment.java index ce099cb..fa85215 100644 --- a/src/main/java/com/sparrowwallet/drongo/dns/DnsPayment.java +++ b/src/main/java/com/sparrowwallet/drongo/dns/DnsPayment.java @@ -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; + } } diff --git a/src/main/java/com/sparrowwallet/drongo/dns/DnsPaymentCache.java b/src/main/java/com/sparrowwallet/drongo/dns/DnsPaymentCache.java index fb6a265..07c56d1 100644 --- a/src/main/java/com/sparrowwallet/drongo/dns/DnsPaymentCache.java +++ b/src/main/java/com/sparrowwallet/drongo/dns/DnsPaymentCache.java @@ -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); } } diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java b/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java index dbada37..0938b80 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java @@ -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) { diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionInput.java b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionInput.java index 1d43d3b..e2a1b67 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionInput.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionInput.java @@ -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; diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionOutput.java b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionOutput.java index 4645d01..2dbb1ea 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionOutput.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionOutput.java @@ -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); diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java index d0e546c..58ce8d2 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java @@ -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 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 outputNodes = new ArrayList<>(); - List> 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); diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTOutput.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTOutput.java index 484d896..7d42a64 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTOutput.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTOutput.java @@ -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 derivedPublicKeys, - Map proprietary, ECKey tapInternalKey, Map dnssecProof) { + Map proprietary, ECKey tapInternalKey, SilentPaymentAddress silentPaymentAddress, Map 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 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 getDnssecProof() { return dnssecProof; } diff --git a/src/main/java/com/sparrowwallet/drongo/silentpayments/InvalidSilentPaymentException.java b/src/main/java/com/sparrowwallet/drongo/silentpayments/InvalidSilentPaymentException.java new file mode 100644 index 0000000..dd1a803 --- /dev/null +++ b/src/main/java/com/sparrowwallet/drongo/silentpayments/InvalidSilentPaymentException.java @@ -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); + } +} diff --git a/src/main/java/com/sparrowwallet/drongo/silentpayments/SilentPayment.java b/src/main/java/com/sparrowwallet/drongo/silentpayments/SilentPayment.java index 898451e..fb0735d 100644 --- a/src/main/java/com/sparrowwallet/drongo/silentpayments/SilentPayment.java +++ b/src/main/java/com/sparrowwallet/drongo/silentpayments/SilentPayment.java @@ -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(); + } } diff --git a/src/main/java/com/sparrowwallet/drongo/silentpayments/SilentPaymentAddress.java b/src/main/java/com/sparrowwallet/drongo/silentpayments/SilentPaymentAddress.java index c2da62c..c8162da 100644 --- a/src/main/java/com/sparrowwallet/drongo/silentpayments/SilentPaymentAddress.java +++ b/src/main/java/com/sparrowwallet/drongo/silentpayments/SilentPaymentAddress.java @@ -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) { diff --git a/src/main/java/com/sparrowwallet/drongo/silentpayments/SilentPaymentUtils.java b/src/main/java/com/sparrowwallet/drongo/silentpayments/SilentPaymentUtils.java index 596cdcd..e458e24 100644 --- a/src/main/java/com/sparrowwallet/drongo/silentpayments/SilentPaymentUtils.java +++ b/src/main/java/com/sparrowwallet/drongo/silentpayments/SilentPaymentUtils.java @@ -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 silentPayments, Map utxos) { + public static void computeOutputAddresses(List silentPayments, Map utxos) throws InvalidSilentPaymentException { ECKey summedPrivateKey = getSummedPrivateKey(utxos.values()); BigInteger inputHash = getInputHash(utxos.keySet(), summedPrivateKey); Map> 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 outpoints, ECKey summedPrivateKey) { + public static BigInteger getInputHash(Set 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 walletNodes) { + public static ECKey getSummedPrivateKey(Collection 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 outpoints) { + public static byte[] getSmallestOutpoint(Set 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")); } diff --git a/src/main/java/com/sparrowwallet/drongo/uri/BitcoinURI.java b/src/main/java/com/sparrowwallet/drongo/uri/BitcoinURI.java index 466b310..1ef2b75 100644 --- a/src/main/java/com/sparrowwallet/drongo/uri/BitcoinURI.java +++ b/src/main/java/com/sparrowwallet/drongo/uri/BitcoinURI.java @@ -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); } diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Payment.java b/src/main/java/com/sparrowwallet/drongo/wallet/Payment.java index 716a8f0..32c1360 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/Payment.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Payment.java @@ -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(); + } } diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java index 0dd67af..d228ec3 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java @@ -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 { private final Map detachedLabels = new HashMap<>(); private WalletConfig walletConfig; private final Map walletTables = new HashMap<>(); + private final Map silentPaymentAddresses = new HashMap<>(); private MixConfig mixConfig; private final Map utxoMixes = new HashMap<>(); private Integer storedBlockHeight; @@ -482,6 +482,31 @@ public class Wallet extends Persistable implements Comparable { 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 { public WalletTransaction createWalletTransaction(List utxoSelectors, List txoFilters, List payments, List opReturns, Set 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 availableTxos = getWalletTxos(txoFilters); @@ -1040,7 +1065,9 @@ public class Wallet extends Persistable implements Comparable { long totalSelectedAmt = selectedUtxos.keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum(); int numSets = selectedUtxoSets.size(); List txPayments = new ArrayList<>(payments); + List outputs = new ArrayList<>(); Set 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 { 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 { txPayments.add(fakeMixPayment); } - List 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 { Map 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 { //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 { } } - 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 { } public void sign(PSBT psbt) throws MnemonicException { - Map signingNodes = getSigningNodes(psbt); + sign(getSigningNodes(psbt)); + } + + public void sign(Map signingNodes) throws MnemonicException { for(Map.Entry signingEntry : signingNodes.entrySet()) { Wallet signingWallet = signingEntry.getValue().getWallet(); for(Keystore keystore : signingWallet.getKeystores()) { @@ -1617,6 +1647,37 @@ public class Wallet extends Persistable implements Comparable { } } + public List computeSilentPaymentOutputs(PSBT psbt, Map signingNodes) throws InvalidSilentPaymentException { + List 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 silentPayments = silentOutputs.stream() + .map(psbtOutput -> new SilentPayment(psbtOutput.getSilentPaymentAddress(), null, psbtOutput.getAmount(), false)).collect(Collectors.toList()); + Map 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 signingNodes = getSigningNodes(psbt); diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/WalletTransaction.java b/src/main/java/com/sparrowwallet/drongo/wallet/WalletTransaction.java index 1ad2c7c..9426237 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/WalletTransaction.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/WalletTransaction.java @@ -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 outputs; private Map> addressNodeMap = new HashMap<>(); - private Map> addressDnsProofMap = new HashMap<>(); - public WalletTransaction(Wallet wallet, Transaction transaction, List utxoSelectors, List> selectedUtxoSets, List payments, long fee) { - this(wallet, transaction, utxoSelectors, selectedUtxoSets, payments, Collections.emptyMap(), fee); + public WalletTransaction(Wallet wallet, Transaction transaction, List utxoSelectors, List> selectedUtxoSets, List payments, List outputs, long fee) { + this(wallet, transaction, utxoSelectors, selectedUtxoSets, payments, outputs, Collections.emptyMap(), fee); } - public WalletTransaction(Wallet wallet, Transaction transaction, List utxoSelectors, List> selectedUtxoSets, List payments, Map changeMap, long fee) { - this(wallet, transaction, utxoSelectors, selectedUtxoSets, payments, changeMap, fee, Collections.emptyMap()); + public WalletTransaction(Wallet wallet, Transaction transaction, List utxoSelectors, List> selectedUtxoSets, List payments, List outputs, Map changeMap, long fee) { + this(wallet, transaction, utxoSelectors, selectedUtxoSets, payments, outputs, changeMap, fee, Collections.emptyMap()); } - public WalletTransaction(Wallet wallet, Transaction transaction, List utxoSelectors, List> selectedUtxoSets, List payments, Map changeMap, long fee, Map inputTransactions) { + public WalletTransaction(Wallet wallet, Transaction transaction, List utxoSelectors, List> selectedUtxoSets, List payments, List outputs, Map changeMap, long fee, Map 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 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> addressNodeMap, Wallet wallet) { @@ -237,46 +243,6 @@ public class WalletTransaction { return walletAddressNodeMap; } - public Map> 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 calculateOutputs() { - List 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 seenIndexes = new HashSet<>(); - for(Map.Entry 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 getDnsSecProof() { + return null; + } } public static class NonAddressOutput extends Output { @@ -306,6 +276,29 @@ public class WalletTransaction { public Payment getPayment() { return payment; } + + public Map 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 { diff --git a/src/test/java/com/sparrowwallet/drongo/silentpayments/SilentPaymentUtilsTest.java b/src/test/java/com/sparrowwallet/drongo/silentpayments/SilentPaymentUtilsTest.java index 5626e5a..6d358d3 100644 --- a/src/test/java/com/sparrowwallet/drongo/silentpayments/SilentPaymentUtilsTest.java +++ b/src/test/java/com/sparrowwallet/drongo/silentpayments/SilentPaymentUtilsTest.java @@ -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 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 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 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 utxos = new LinkedHashMap<>(); + Map utxos = new LinkedHashMap<>(); Map 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 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 utxos = new LinkedHashMap<>(); + Map 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 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 utxos = new LinkedHashMap<>(); + Map utxos = new LinkedHashMap<>(); Map 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 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 utxos = new LinkedHashMap<>(); - Map 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 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 utxos = new LinkedHashMap<>(); + Map utxos = new LinkedHashMap<>(); Map 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 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 utxos = new LinkedHashMap<>(); + Map utxos = new LinkedHashMap<>(); Map 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 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 utxos = new LinkedHashMap<>(); + Map utxos = new LinkedHashMap<>(); Map 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 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 utxos = new LinkedHashMap<>(); + Map utxos = new LinkedHashMap<>(); Map 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 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 utxos = new LinkedHashMap<>(); + Map utxos = new LinkedHashMap<>(); Map 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 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 segwitPrivateKeys = new LinkedHashMap<>(); - Map utxos = new LinkedHashMap<>(); + Map 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 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 segwitPrivateKeys = new LinkedHashMap<>(); - Map utxos = new LinkedHashMap<>(); + Map 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 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 utxos = new LinkedHashMap<>(); + Map utxos = new LinkedHashMap<>(); Map 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 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 utxos = new LinkedHashMap<>(); + Map utxos = new LinkedHashMap<>(); Map 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 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()));