Compare commits
No commits in common. "master" and "sp" have entirely different histories.
@ -14,7 +14,6 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@ -94,15 +93,6 @@ public class Utils {
|
||||
return ((st > 0) || (len < bytes.length)) ? Arrays.copyOfRange(bytes, st, len) : bytes;
|
||||
}
|
||||
|
||||
public static boolean isSecureUrl(URI uri) {
|
||||
if(uri == null || uri.getScheme() == null || uri.getHost() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String scheme = uri.getScheme().toLowerCase(Locale.ROOT);
|
||||
return "https".equals(scheme) || ("http".equals(scheme) && uri.getHost().toLowerCase(Locale.ROOT).endsWith(".onion"));
|
||||
}
|
||||
|
||||
public static String bytesToHex(byte[] bytes) {
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
for ( int j = 0; j < bytes.length; j++ ) {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package com.sparrowwallet.drongo.crypto;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
@ -16,10 +15,6 @@ import java.util.*;
|
||||
import static com.sparrowwallet.drongo.protocol.ScriptType.P2TR;
|
||||
|
||||
public class Bip322 {
|
||||
public static final String SIMPLE_PREFIX = "smp";
|
||||
public static final String FULL_PREFIX = "ful";
|
||||
public static final String FULL_POF_PREFIX = "pof";
|
||||
|
||||
public static String signMessageBip322(ScriptType scriptType, String message, ECKey privKey) {
|
||||
checkScriptType(scriptType);
|
||||
|
||||
@ -41,7 +36,6 @@ public class Bip322 {
|
||||
TransactionOutput utxoOutput = toSpend.getOutputs().getFirst();
|
||||
|
||||
PSBT psbt = new PSBT(toSign);
|
||||
psbt.setGenericSignedMessage(message);
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().getFirst();
|
||||
psbtInput.setWitnessUtxo(utxoOutput);
|
||||
psbtInput.setSigHash(SigHash.ALL);
|
||||
@ -49,46 +43,6 @@ public class Bip322 {
|
||||
return psbt;
|
||||
}
|
||||
|
||||
public static PSBT getBip322PsbtSp(Address address, String message, byte[] silentPaymentsTweak, Map<ECKey, KeyDerivation> spendDerivations) {
|
||||
if(silentPaymentsTweak == null) {
|
||||
throw new IllegalArgumentException("Silent payments tweak is required");
|
||||
}
|
||||
|
||||
PSBT psbt = getBip322Psbt(P2TR, address, message);
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().getFirst();
|
||||
psbtInput.setSilentPaymentsTweak(silentPaymentsTweak);
|
||||
if(spendDerivations != null && !spendDerivations.isEmpty()) {
|
||||
psbtInput.getSilentPaymentsSpendDerivations().putAll(spendDerivations);
|
||||
}
|
||||
|
||||
return psbt;
|
||||
}
|
||||
|
||||
public static String signMessageBip322Sp(Address address, String message, ECKey spendPrivKey, byte[] silentPaymentsTweak) {
|
||||
PSBT psbt = getBip322PsbtSp(address, message, silentPaymentsTweak, Collections.emptyMap());
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().getFirst();
|
||||
|
||||
if(!psbtInput.signSilentPayments(spendPrivKey)) {
|
||||
throw new IllegalStateException("Failed to sign BIP322 PSBT with silent payments tweak");
|
||||
}
|
||||
|
||||
return getBip322SignatureFromPsbtSp(psbt);
|
||||
}
|
||||
|
||||
public static String getBip322SignatureFromPsbtSp(PSBT signedPsbt) {
|
||||
PSBTInput psbtInput = signedPsbt.getPsbtInputs().getFirst();
|
||||
TransactionSignature signature = psbtInput.getTapKeyPathSignature();
|
||||
if(signature == null) {
|
||||
throw new IllegalArgumentException("PSBT does not contain a taproot keypath signature");
|
||||
}
|
||||
|
||||
Transaction finalizeTransaction = new Transaction();
|
||||
TransactionWitness witness = new TransactionWitness(finalizeTransaction, signature);
|
||||
finalizeTransaction.addInput(Sha256Hash.ZERO_HASH, 0, new Script(new byte[0]), witness);
|
||||
|
||||
return SIMPLE_PREFIX + Base64.getEncoder().encodeToString(witness.toByteArray());
|
||||
}
|
||||
|
||||
public static String getBip322SignatureFromPsbt(ScriptType scriptType, PSBT signedPsbt, ECKey pubKey) {
|
||||
checkScriptType(scriptType);
|
||||
|
||||
@ -105,28 +59,19 @@ public class Bip322 {
|
||||
TransactionWitness witness = psbtInput.isTaproot() ? new TransactionWitness(finalizeTransaction, signature) : new TransactionWitness(finalizeTransaction, pubKey, signature);
|
||||
TransactionInput finalizedTxInput = finalizeTransaction.addInput(Sha256Hash.ZERO_HASH, 0, scriptSig, witness);
|
||||
|
||||
return SIMPLE_PREFIX + Base64.getEncoder().encodeToString(finalizedTxInput.getWitness().toByteArray());
|
||||
return Base64.getEncoder().encodeToString(finalizedTxInput.getWitness().toByteArray());
|
||||
}
|
||||
|
||||
public static boolean verifyMessageBip322(ScriptType scriptType, Address address, String message, String signatureBase64) throws SignatureException {
|
||||
checkScriptType(scriptType);
|
||||
|
||||
String trimmed = signatureBase64.trim();
|
||||
if(trimmed.isEmpty()) {
|
||||
if(signatureBase64.trim().isEmpty()) {
|
||||
throw new SignatureException("Provided signature is empty.");
|
||||
}
|
||||
|
||||
if(trimmed.startsWith(FULL_PREFIX) || trimmed.startsWith(FULL_POF_PREFIX)) {
|
||||
throw new SignatureException("Only the simple BIP322 signature variant is supported.");
|
||||
}
|
||||
|
||||
if(trimmed.startsWith(SIMPLE_PREFIX)) {
|
||||
trimmed = trimmed.substring(SIMPLE_PREFIX.length());
|
||||
}
|
||||
|
||||
byte[] signatureEncoded;
|
||||
try {
|
||||
signatureEncoded = Base64.getDecoder().decode(trimmed);
|
||||
signatureEncoded = Base64.getDecoder().decode(signatureBase64);
|
||||
} catch(IllegalArgumentException e) {
|
||||
throw new SignatureException("Could not decode base64 signature", e);
|
||||
}
|
||||
|
||||
@ -13,7 +13,6 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -33,7 +32,6 @@ public class PSBT {
|
||||
public static final byte PSBT_GLOBAL_TX_MODIFIABLE = 0x06;
|
||||
public static final byte PSBT_GLOBAL_SP_ECDH_SHARE = 0x07;
|
||||
public static final byte PSBT_GLOBAL_SP_DLEQ = 0x08;
|
||||
public static final byte PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE = 0x09;
|
||||
public static final byte PSBT_GLOBAL_VERSION = (byte)0xfb;
|
||||
public static final byte PSBT_GLOBAL_PROPRIETARY = (byte)0xfc;
|
||||
|
||||
@ -52,7 +50,6 @@ public class PSBT {
|
||||
|
||||
private Integer version = null;
|
||||
private final Map<ExtendedKey, KeyDerivation> extendedPublicKeys = new LinkedHashMap<>();
|
||||
private String genericSignedMessage = null;
|
||||
private final Map<String, String> globalProprietary = new LinkedHashMap<>();
|
||||
|
||||
//PSBTv0-only fields
|
||||
@ -437,11 +434,6 @@ public class PSBT {
|
||||
this.silentPaymentsDLEQProofs.put(proofScanKey, dleqProof);
|
||||
log.debug("PSBT global silent payments DLEQ proof for scan key: " + Utils.bytesToHex(entry.getKeyData()));
|
||||
break;
|
||||
case PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE:
|
||||
entry.checkOneByteKey();
|
||||
this.genericSignedMessage = new String(entry.getData(), StandardCharsets.UTF_8);
|
||||
log.debug("PSBT global generic signed message: " + genericSignedMessage);
|
||||
break;
|
||||
case PSBT_GLOBAL_PROPRIETARY:
|
||||
globalProprietary.put(Utils.bytesToHex(entry.getKeyData()), Utils.bytesToHex(entry.getData()));
|
||||
log.debug("PSBT global proprietary data: " + Utils.bytesToHex(entry.getData()));
|
||||
@ -671,40 +663,6 @@ public class PSBT {
|
||||
}
|
||||
}
|
||||
|
||||
public void verifySigHashes() throws PSBTSignatureException {
|
||||
PSBTSignatureException worst = null;
|
||||
int worstSeverity = 0;
|
||||
for(PSBTInput input : psbtInputs) {
|
||||
try {
|
||||
input.verifySigHash();
|
||||
} catch(PSBTSignatureException e) {
|
||||
int severity = sigHashSeverity(input.getSigHash());
|
||||
if(severity > worstSeverity) {
|
||||
worstSeverity = severity;
|
||||
worst = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(worst != null) {
|
||||
throw worst;
|
||||
}
|
||||
}
|
||||
|
||||
private static int sigHashSeverity(SigHash sigHash) {
|
||||
if(sigHash == null) {
|
||||
return 0;
|
||||
}
|
||||
return switch(sigHash) {
|
||||
case NONE, ANYONECANPAY_NONE -> 5;
|
||||
case ANYONECANPAY_SINGLE -> 4;
|
||||
case SINGLE -> 3;
|
||||
case ANYONECANPAY_ALL -> 2;
|
||||
case ANYONECANPAY -> 1;
|
||||
default -> 0;
|
||||
};
|
||||
}
|
||||
|
||||
public void validateSilentPayments(Transaction extractedTransaction) throws PSBTProofException {
|
||||
Map<HashIndex, Script> spentScriptPubKeys = getPsbtInputs().stream()
|
||||
.collect(Collectors.toMap(psbtInput -> new HashIndex(psbtInput.getPrevTxid(), psbtInput.getPrevIndex()), psbtInput -> psbtInput.getUtxo().getScript()));
|
||||
@ -917,10 +875,6 @@ public class PSBT {
|
||||
entries.add(populateEntry(PSBT_GLOBAL_VERSION, null, versionBytes));
|
||||
}
|
||||
|
||||
if(genericSignedMessage != null) {
|
||||
entries.add(populateEntry(PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE, null, genericSignedMessage.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
for(Map.Entry<String, String> entry : globalProprietary.entrySet()) {
|
||||
entries.add(populateEntry(PSBT_GLOBAL_PROPRIETARY, Utils.hexToBytes(entry.getKey()), Utils.hexToBytes(entry.getValue())));
|
||||
}
|
||||
@ -978,21 +932,6 @@ public class PSBT {
|
||||
PSBT verificationCopy = this.copy();
|
||||
verificationCopy.combine(psbt);
|
||||
verificationCopy.verifySignatures();
|
||||
verifyCombinedSigHashes(verificationCopy);
|
||||
}
|
||||
|
||||
private void verifyCombinedSigHashes(PSBT verificationCopy) throws PSBTSignatureException {
|
||||
for(int i = 0; i < getPsbtInputs().size(); i++) {
|
||||
PSBTInput thisInput = getPsbtInputs().get(i);
|
||||
PSBTInput otherInput = verificationCopy.getPsbtInputs().get(i);
|
||||
if(sigHashSeverity(otherInput.getSigHash()) > sigHashSeverity(thisInput.getSigHash())) {
|
||||
try {
|
||||
otherInput.verifySigHash();
|
||||
} catch(PSBTSignatureException e) {
|
||||
throw new PSBTSignatureException("Combined PSBT would change sighash: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void combine(PSBT... psbts) {
|
||||
@ -1287,14 +1226,6 @@ public class PSBT {
|
||||
return globalProprietary;
|
||||
}
|
||||
|
||||
public String getGenericSignedMessage() {
|
||||
return genericSignedMessage;
|
||||
}
|
||||
|
||||
public void setGenericSignedMessage(String genericSignedMessage) {
|
||||
this.genericSignedMessage = genericSignedMessage;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return Utils.bytesToHex(serialize());
|
||||
}
|
||||
|
||||
@ -923,12 +923,6 @@ public class PSBTInput {
|
||||
if(getNonWitnessUtxo() != null || getWitnessUtxo() != null) {
|
||||
Script signingScript = getSigningScript();
|
||||
if(signingScript != null) {
|
||||
if((localSigHash == SigHash.SINGLE || localSigHash == SigHash.ANYONECANPAY_SINGLE) && index >= psbt.getTransaction().getOutputs().size()
|
||||
&& Arrays.asList(NON_WITNESS_TYPES).contains(getScriptType())) {
|
||||
throw new IllegalStateException("Refusing to sign SIGHASH_SINGLE on legacy input " + index
|
||||
+ " with only " + psbt.getTransaction().getOutputs().size() + " output(s) as it would produce a re-broadcastable signature");
|
||||
}
|
||||
|
||||
Sha256Hash hash = getHashForSignature(signingScript, localSigHash);
|
||||
TransactionSignature.Type type = isTaproot() ? SCHNORR : ECDSA;
|
||||
TransactionSignature transactionSignature = psbtInputSigner.sign(hash, localSigHash, type);
|
||||
@ -947,27 +941,6 @@ public class PSBTInput {
|
||||
return false;
|
||||
}
|
||||
|
||||
void verifySigHash() throws PSBTSignatureException {
|
||||
if(sigHash == null || sigHash == SigHash.ALL || sigHash == SigHash.DEFAULT) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch(sigHash) {
|
||||
case NONE:
|
||||
throw new PSBTSignatureException("Input " + index + " requests SIGHASH_NONE. The signature does not commit to any of the outputs, and can be re-used on a transaction with completely different outputs.");
|
||||
case ANYONECANPAY_NONE:
|
||||
throw new PSBTSignatureException("Input " + index + " requests SIGHASH_NONE | ANYONECANPAY. The signature commits to neither inputs nor outputs and can be re-used in nearly any transaction.");
|
||||
case ANYONECANPAY_SINGLE:
|
||||
throw new PSBTSignatureException("Input " + index + " requests SIGHASH_SINGLE | ANYONECANPAY. The signature only commits to one output, and other inputs may be added after signing.");
|
||||
case SINGLE:
|
||||
throw new PSBTSignatureException("Input " + index + " requests SIGHASH_SINGLE. The signature only commits to the output at the same index, allowing other outputs to be added or modified after signing.");
|
||||
case ANYONECANPAY_ALL:
|
||||
throw new PSBTSignatureException("Input " + index + " requests SIGHASH_ALL | ANYONECANPAY. Other inputs may be added to the transaction after signing, potentially redirecting value through fees.");
|
||||
case ANYONECANPAY:
|
||||
throw new PSBTSignatureException("Input " + index + " requests a non-standard ANYONECANPAY sighash with no base type. The resulting signature has unpredictable commitment semantics.");
|
||||
}
|
||||
}
|
||||
|
||||
boolean verifySignatures() throws PSBTSignatureException {
|
||||
SigHash localSigHash = getSigHash();
|
||||
if(localSigHash == null) {
|
||||
|
||||
@ -94,17 +94,4 @@ public class SilentPaymentAddress {
|
||||
buffer.put(spendAddress.getPubKey());
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
public static SilentPaymentAddress fromBytes(byte[] bytes) {
|
||||
if(bytes.length != 67) {
|
||||
throw new IllegalArgumentException("Silent payment address must be 67 bytes");
|
||||
}
|
||||
int version = bytes[0] & 0xff;
|
||||
if(version != VERSION) {
|
||||
throw new UnsupportedOperationException("Unsupported silent payments address version " + version);
|
||||
}
|
||||
ECKey scanPubKey = ECKey.fromPublicOnly(Arrays.copyOfRange(bytes, 1, 34));
|
||||
ECKey spendPubKey = ECKey.fromPublicOnly(Arrays.copyOfRange(bytes, 34, 67));
|
||||
return new SilentPaymentAddress(scanPubKey, spendPubKey);
|
||||
}
|
||||
}
|
||||
|
||||
@ -331,20 +331,6 @@ public class SilentPaymentUtils {
|
||||
return ECKey.fromPrivate(summedPrivKey);
|
||||
}
|
||||
|
||||
public static ECKey getInputPublicKey(WalletNode walletNode) {
|
||||
if(!walletNode.getWallet().canSendSilentPayments()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ECKey rawKey = walletNode.getPubKey();
|
||||
ECKey publicKey = walletNode.getWallet().getScriptType().getOutputKey(walletNode.getWallet().getPolicyType(), rawKey);
|
||||
if(walletNode.getWallet().getScriptType() == P2TR && publicKey.hasOddYCoord()) {
|
||||
publicKey = publicKey.negate();
|
||||
}
|
||||
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public static ECKey getSummedPublicKey(Collection<ECKey> publicKeys) {
|
||||
ECKey summedKey = null;
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package com.sparrowwallet.drongo.uri;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
||||
@ -276,10 +275,10 @@ public class BitcoinURI {
|
||||
if(payjoinUrl != null) {
|
||||
try {
|
||||
URI uri = new URI(payjoinUrl);
|
||||
if(Utils.isSecureUrl(uri)) {
|
||||
if(uri.getScheme().equals("https") || uri.getHost().endsWith(".onion")) {
|
||||
return uri;
|
||||
} else {
|
||||
log.error("Insecure payjoin URL provided, must be https or http .onion: " + payjoinUrl);
|
||||
log.error("Insecure payjoin URL provided, must be https or .onion: " + payjoinUrl);
|
||||
}
|
||||
} catch(URISyntaxException e) {
|
||||
log.error("Invalid payjoin URL provided", e);
|
||||
|
||||
@ -13,7 +13,6 @@ 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.psbt.PSBTProofException;
|
||||
import com.sparrowwallet.drongo.silentpayments.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@ -498,24 +497,21 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||
}
|
||||
|
||||
public SilentPaymentAddress getSilentPaymentAddress(Address address) {
|
||||
return resolveMasterWallet().silentPaymentAddresses.get(address);
|
||||
return silentPaymentAddresses.get(address);
|
||||
}
|
||||
|
||||
public void addSilentPaymentAddress(Address address, SilentPaymentAddress silentPaymentAddress) {
|
||||
resolveMasterWallet().silentPaymentAddresses.put(address, silentPaymentAddress);
|
||||
}
|
||||
|
||||
public Map<Address, SilentPaymentAddress> getSilentPaymentAddresses() {
|
||||
return resolveMasterWallet().silentPaymentAddresses;
|
||||
private void addSilentPaymentAddress(Address address, SilentPaymentAddress silentPaymentAddress) {
|
||||
silentPaymentAddresses.put(address, silentPaymentAddress);
|
||||
}
|
||||
|
||||
public void clearSilentPaymentAddress(Address address) {
|
||||
resolveMasterWallet().silentPaymentAddresses.remove(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 -> getSilentPaymentAddress(address) != null);
|
||||
.filter(Objects::nonNull).anyMatch(address -> wallet.getSilentPaymentAddress(address) != null);
|
||||
}
|
||||
|
||||
public boolean isSafeToAddInputsOrOutputs(BlockTransaction blockTransaction) {
|
||||
@ -605,20 +601,6 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||
this.birthHeight = birthHeight;
|
||||
}
|
||||
|
||||
public int getNeededScanStart() {
|
||||
if(storedBlockHeight != null && storedBlockHeight > 0) {
|
||||
return Math.max(0, storedBlockHeight - BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM);
|
||||
}
|
||||
if(birthHeight != null) {
|
||||
return Math.max(0, birthHeight - BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM);
|
||||
}
|
||||
if(birthDate != null) {
|
||||
return (int)(birthDate.getTime() / 1000L);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public boolean isMasterWallet() {
|
||||
return masterWallet == null;
|
||||
}
|
||||
@ -631,10 +613,6 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||
this.masterWallet = masterWallet;
|
||||
}
|
||||
|
||||
public Wallet resolveMasterWallet() {
|
||||
return isNested() ? getMasterWallet() : this;
|
||||
}
|
||||
|
||||
public Wallet getChildWallet(String name) {
|
||||
return childWallets.stream().filter(wallet -> wallet.getName().equals(name)).findFirst().orElse(null);
|
||||
}
|
||||
@ -1782,16 +1760,6 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||
}
|
||||
|
||||
public void sign(PSBT psbt) throws MnemonicException {
|
||||
if(psbt.getPsbtOutputs().stream().anyMatch(o -> o.getSilentPaymentAddress() != null)) {
|
||||
List<PSBTInput> psbtInputs = psbt.getPsbtInputs();
|
||||
for(int i = 0; i < psbtInputs.size(); i++) {
|
||||
SigHash inputSigHash = psbtInputs.get(i).getSigHash();
|
||||
if(inputSigHash != null && inputSigHash != SigHash.ALL && inputSigHash != SigHash.DEFAULT) {
|
||||
throw new IllegalStateException("Silent payment outputs require SIGHASH_ALL/DEFAULT signatures. Input at index " + i + " has sighash type: " + inputSigHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sign(getSigningNodes(psbt));
|
||||
}
|
||||
|
||||
@ -1816,113 +1784,14 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the subset of PSBT_OUT_SP_V0_INFO entries whose claimed silent payment address
|
||||
* actually derives the PSBT output script under BIP-352, as proven by the PSBT's BIP-375
|
||||
* ECDH shares and DLEQ proofs (global or per-input form) against this wallet's input public
|
||||
* keys. Verification uses public keys only and works for both software and hardware keystores.
|
||||
* Returns an empty map if any input is not from this wallet, if BIP-375 metadata is missing
|
||||
* or inconsistent, if any DLEQ proof fails to verify, or if any claimed script does not
|
||||
* match the BIP-352 derivation.
|
||||
*/
|
||||
public Map<Address, SilentPaymentAddress> verifySilentPaymentOutputs(PSBT psbt) {
|
||||
Map<Address, SilentPaymentAddress> verified = new LinkedHashMap<>();
|
||||
if(psbt == null) {
|
||||
return verified;
|
||||
}
|
||||
|
||||
List<PSBTOutput> spOutputs = new ArrayList<>();
|
||||
for(PSBTOutput psbtOutput : psbt.getPsbtOutputs()) {
|
||||
if(psbtOutput.getSilentPaymentAddress() == null) {
|
||||
continue;
|
||||
}
|
||||
Script script = psbtOutput.getScript();
|
||||
if(script == null || script.getToAddress() == null) {
|
||||
return verified;
|
||||
}
|
||||
spOutputs.add(psbtOutput);
|
||||
}
|
||||
|
||||
if(spOutputs.isEmpty()) {
|
||||
return verified;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<PSBTInput, WalletNode> signingNodes = getSigningNodes(psbt);
|
||||
if(psbt.getPsbtInputs().size() != signingNodes.size()) {
|
||||
return verified;
|
||||
}
|
||||
|
||||
Map<TransactionInput, ECKey> inputPublicKeys = new LinkedHashMap<>();
|
||||
Transaction transaction = psbt.getTransaction();
|
||||
for(int i = 0; i < psbt.getPsbtInputs().size(); i++) {
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().get(i);
|
||||
WalletNode node = signingNodes.get(psbtInput);
|
||||
ECKey publicKey = SilentPaymentUtils.getInputPublicKey(node);
|
||||
if(publicKey == null) {
|
||||
return verified;
|
||||
}
|
||||
inputPublicKeys.put(transaction.getInputs().get(i), publicKey);
|
||||
}
|
||||
|
||||
psbt.validateSilentPayments(inputPublicKeys);
|
||||
|
||||
for(PSBTOutput psbtOutput : spOutputs) {
|
||||
verified.put(psbtOutput.getScript().getToAddress(), psbtOutput.getSilentPaymentAddress());
|
||||
}
|
||||
} catch(Exception e) {
|
||||
return new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
return verified;
|
||||
}
|
||||
|
||||
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()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if(psbt.getPsbtInputs().size() != signingNodes.size()) {
|
||||
throw new InvalidSilentPaymentException("All inputs must be from wallet to calculate silent payment addresses");
|
||||
}
|
||||
|
||||
List<PSBTOutput> preComputedOutputs = new ArrayList<>();
|
||||
List<PSBTOutput> computeOutputs = new ArrayList<>();
|
||||
for(PSBTOutput silentOutput : silentOutputs) {
|
||||
Script script = silentOutput.getScript();
|
||||
if(script != null && script.getToAddress() != null) {
|
||||
preComputedOutputs.add(silentOutput);
|
||||
} else {
|
||||
computeOutputs.add(silentOutput);
|
||||
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> results = new ArrayList<>();
|
||||
|
||||
if(!preComputedOutputs.isEmpty()) {
|
||||
Map<TransactionInput, ECKey> inputPublicKeys = new LinkedHashMap<>();
|
||||
Transaction transaction = psbt.getTransaction();
|
||||
for(int i = 0; i < psbt.getPsbtInputs().size(); i++) {
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().get(i);
|
||||
ECKey publicKey = SilentPaymentUtils.getInputPublicKey(signingNodes.get(psbtInput));
|
||||
if(publicKey == null) {
|
||||
throw new InvalidSilentPaymentException("Cannot derive input public key for silent payment verification");
|
||||
}
|
||||
inputPublicKeys.put(transaction.getInputs().get(i), publicKey);
|
||||
}
|
||||
try {
|
||||
psbt.validateSilentPayments(inputPublicKeys);
|
||||
} catch(PSBTProofException e) {
|
||||
throw new InvalidSilentPaymentException("Silent payment metadata in PSBT failed BIP-375 verification: " + e.getMessage());
|
||||
}
|
||||
for(PSBTOutput silentOutput : preComputedOutputs) {
|
||||
results.add(new SilentPayment(silentOutput.getSilentPaymentAddress(), silentOutput.getScript().getToAddress(), null, silentOutput.getAmount(), false));
|
||||
}
|
||||
}
|
||||
|
||||
if(!computeOutputs.isEmpty()) {
|
||||
List<SilentPayment> silentPayments = computeOutputs.stream()
|
||||
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));
|
||||
@ -1932,8 +1801,8 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||
psbt.getSilentPaymentsDLEQProofs().put(key, ecdhShareAndProof.dleqProof());
|
||||
});
|
||||
|
||||
for(int i = 0; i < computeOutputs.size(); i++) {
|
||||
PSBTOutput silentOutput = computeOutputs.get(i);
|
||||
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");
|
||||
@ -1941,12 +1810,13 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||
|
||||
Script outputScript = silentPayment.getAddress().getOutputScript();
|
||||
silentOutput.setScript(outputScript);
|
||||
addSilentPaymentAddress(silentPayment.getAddress(), silentPayment.getSilentPaymentAddress());
|
||||
}
|
||||
|
||||
results.addAll(silentPayments);
|
||||
return silentPayments;
|
||||
}
|
||||
|
||||
return results;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public void finalise(PSBT psbt) {
|
||||
@ -2012,80 +1882,12 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||
psbtInput.setFinalScriptSig(finalizedTxInput.getScriptSig());
|
||||
psbtInput.setFinalScriptWitness(finalizedTxInput.getWitness());
|
||||
psbtInput.clearNonFinalFields();
|
||||
} else if(signingNode == null) {
|
||||
finaliseExternalInput(psbtInput, utxo, signaturesAvailable);
|
||||
}
|
||||
}
|
||||
|
||||
psbt.getPsbtOutputs().forEach(PSBTOutput::clearNonFinalFields);
|
||||
}
|
||||
|
||||
private void finaliseExternalInput(PSBTInput psbtInput, TransactionOutput utxo, int signaturesAvailable) {
|
||||
Script signingScript = psbtInput.getSigningScript();
|
||||
ScriptType inputScriptType = psbtInput.getScriptType();
|
||||
if(signingScript == null || inputScriptType == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int inputThreshold;
|
||||
try {
|
||||
inputThreshold = signingScript.getNumRequiredSignatures();
|
||||
} catch(NonStandardScriptException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(signaturesAvailable < inputThreshold) {
|
||||
return;
|
||||
}
|
||||
|
||||
Transaction transaction = new Transaction();
|
||||
TransactionInput finalizedTxInput;
|
||||
|
||||
if(MULTISIG.isScriptType(signingScript)) {
|
||||
ECKey[] scriptPubKeys = MULTISIG.getPublicKeysFromScript(signingScript);
|
||||
Map<ECKey, TransactionSignature> pubKeySignatures = new TreeMap<>(new ECKey.LexicographicECKeyComparator());
|
||||
for(ECKey pubKey : scriptPubKeys) {
|
||||
pubKeySignatures.put(pubKey, psbtInput.getPartialSignature(pubKey));
|
||||
}
|
||||
|
||||
long matched = pubKeySignatures.values().stream().filter(Objects::nonNull).count();
|
||||
if(matched < inputThreshold) {
|
||||
return;
|
||||
}
|
||||
|
||||
finalizedTxInput = inputScriptType.addMultisigSpendingInput(PolicyType.MULTI_HD, transaction, utxo, inputThreshold, pubKeySignatures);
|
||||
} else if(inputThreshold == 1) {
|
||||
ECKey pubKey;
|
||||
TransactionSignature signature;
|
||||
PolicyType policyType;
|
||||
if(psbtInput.isTaproot()) {
|
||||
//The witness program is the tweaked output key - pass it as SINGLE_SP so getOutputKey does not tweak again
|
||||
policyType = PolicyType.SINGLE_SP;
|
||||
pubKey = P2TR.getPublicKeyFromScript(utxo.getScript());
|
||||
signature = psbtInput.getTapKeyPathSignature();
|
||||
} else if(psbtInput.getPartialSignatures().size() == 1) {
|
||||
policyType = PolicyType.SINGLE_HD;
|
||||
Map.Entry<ECKey, TransactionSignature> entry = psbtInput.getPartialSignatures().entrySet().iterator().next();
|
||||
pubKey = entry.getKey();
|
||||
signature = entry.getValue();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if(signature == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
finalizedTxInput = inputScriptType.addSpendingInput(policyType, transaction, utxo, pubKey, signature);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
psbtInput.setFinalScriptSig(finalizedTxInput.getScriptSig());
|
||||
psbtInput.setFinalScriptWitness(finalizedTxInput.getWitness());
|
||||
psbtInput.clearNonFinalFields();
|
||||
}
|
||||
|
||||
public BitcoinUnit getAutoUnit() {
|
||||
for(KeyPurpose keyPurpose : KeyPurpose.values()) {
|
||||
for(WalletNode addressNode : getNode(keyPurpose).getChildren()) {
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Comment: Hostname:
|
||||
Version: Hockeypuck 2.2
|
||||
|
||||
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
|
||||
yQdYDjoIbgfDCQ7KTnaRHQ==
|
||||
=32hh
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -1,48 +0,0 @@
|
||||
package com.sparrowwallet.drongo;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
public class UtilsTest {
|
||||
@Test
|
||||
public void isSecureUrlAcceptsHttps() throws Exception {
|
||||
Assertions.assertTrue(Utils.isSecureUrl(new URI("https://example.com/callback")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isSecureUrlAcceptsUppercaseHttps() throws Exception {
|
||||
Assertions.assertTrue(Utils.isSecureUrl(new URI("HTTPS://example.com/callback")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isSecureUrlAcceptsHttpOnion() throws Exception {
|
||||
Assertions.assertTrue(Utils.isSecureUrl(new URI("http://abcdefghijklmnopqrstuvwxyzabcdefghijklmnop.onion/callback")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isSecureUrlRejectsHttpClearnet() throws Exception {
|
||||
Assertions.assertFalse(Utils.isSecureUrl(new URI("http://example.com/callback")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isSecureUrlRejectsNonHttpOnion() throws Exception {
|
||||
Assertions.assertFalse(Utils.isSecureUrl(new URI("ftp://abcdefghijklmnopqrstuvwxyzabcdefghijklmnop.onion/callback")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isSecureUrlRejectsOpaqueHttps() throws Exception {
|
||||
Assertions.assertFalse(Utils.isSecureUrl(new URI("https:opaque")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isSecureUrlRejectsSchemeless() throws Exception {
|
||||
Assertions.assertFalse(Utils.isSecureUrl(new URI("callback")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isSecureUrlRejectsNull() {
|
||||
Assertions.assertFalse(Utils.isSecureUrl(null));
|
||||
}
|
||||
}
|
||||
@ -30,10 +30,10 @@ public class Bip322Test {
|
||||
Assertions.assertEquals("bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", address.toString());
|
||||
|
||||
String signature = Bip322.signMessageBip322(ScriptType.P2WPKH, "", privKey);
|
||||
Assertions.assertEquals("smpAkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", signature);
|
||||
Assertions.assertEquals("AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", signature);
|
||||
|
||||
String signature2 = Bip322.signMessageBip322(ScriptType.P2WPKH, "Hello World", privKey);
|
||||
Assertions.assertEquals("smpAkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", signature2);
|
||||
Assertions.assertEquals("AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", signature2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -49,7 +49,7 @@ public class Bip322Test {
|
||||
public void verifyMessageBip322() throws InvalidAddressException, SignatureException {
|
||||
Address address = Address.fromString("bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l");
|
||||
String message1 = "";
|
||||
String signature1 = "smpAkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=";
|
||||
String signature1 = "AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=";
|
||||
|
||||
Assertions.assertTrue(Bip322.verifyMessageBip322(ScriptType.P2WPKH, address, message1, signature1));
|
||||
|
||||
@ -57,20 +57,10 @@ public class Bip322Test {
|
||||
String signature2 = "AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=";
|
||||
Assertions.assertTrue(Bip322.verifyMessageBip322(ScriptType.P2WPKH, address, message2, signature2));
|
||||
|
||||
String signature3 = "smpAkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy";
|
||||
String signature3 = "AkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy";
|
||||
Assertions.assertTrue(Bip322.verifyMessageBip322(ScriptType.P2WPKH, address, message2, signature3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyMessageBip322RejectsFullVariant() throws InvalidAddressException {
|
||||
Address address = Address.fromString("bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l");
|
||||
String fullSignature = "fulAkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=";
|
||||
Assertions.assertThrows(SignatureException.class, () -> Bip322.verifyMessageBip322(ScriptType.P2WPKH, address, "", fullSignature));
|
||||
|
||||
String pofSignature = "pofAkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=";
|
||||
Assertions.assertThrows(SignatureException.class, () -> Bip322.verifyMessageBip322(ScriptType.P2WPKH, address, "", pofSignature));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void signMessageBip322Taproot() {
|
||||
ECKey privKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
|
||||
@ -78,7 +68,7 @@ public class Bip322Test {
|
||||
Assertions.assertEquals("bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3", address.toString());
|
||||
|
||||
String signature = Bip322.signMessageBip322(ScriptType.P2TR, "Hello World", privKey);
|
||||
Assertions.assertEquals("smpAUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==", signature);
|
||||
Assertions.assertEquals("AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==", signature);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -88,11 +78,9 @@ public class Bip322Test {
|
||||
Assertions.assertEquals("bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3", address.toString());
|
||||
|
||||
String message1 = "Hello World";
|
||||
String signaturePrefixed = "smpAUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==";
|
||||
Assertions.assertTrue(Bip322.verifyMessageBip322(ScriptType.P2TR, address, message1, signaturePrefixed));
|
||||
String signature1 = "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==";
|
||||
|
||||
String signatureLegacy = "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==";
|
||||
Assertions.assertTrue(Bip322.verifyMessageBip322(ScriptType.P2TR, address, message1, signatureLegacy));
|
||||
Assertions.assertTrue(Bip322.verifyMessageBip322(ScriptType.P2TR, address, message1, signature1));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -127,20 +115,6 @@ public class Bip322Test {
|
||||
Assertions.assertNotNull(psbtInput.getWitnessUtxo());
|
||||
Assertions.assertEquals(SigHash.ALL, psbtInput.getSigHash());
|
||||
Assertions.assertEquals(0, psbt.getTransaction().getVersion());
|
||||
Assertions.assertEquals("Hello World", psbt.getGenericSignedMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getBip322PsbtMessageRoundTrip() throws com.sparrowwallet.drongo.psbt.PSBTParseException {
|
||||
ECKey privKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
|
||||
Address address = ScriptType.P2WPKH.getAddress(PolicyType.SINGLE_HD, privKey);
|
||||
|
||||
String message = "UTF-8 support: öäüéàè 测试文本 😄";
|
||||
PSBT psbt = Bip322.getBip322Psbt(ScriptType.P2WPKH, address, message);
|
||||
Assertions.assertEquals(message, psbt.getGenericSignedMessage());
|
||||
|
||||
PSBT roundTrip = new PSBT(psbt.serialize(), false);
|
||||
Assertions.assertEquals(message, roundTrip.getGenericSignedMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -193,54 +167,12 @@ public class Bip322Test {
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> Bip322.getBip322SignatureFromPsbt(ScriptType.P2WPKH, psbt, pubKey));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void signMessageBip322Sp() throws SignatureException {
|
||||
ECKey spendPrivKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
|
||||
byte[] tweak = Utils.hexToBytes("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
|
||||
|
||||
ECKey spendPubKey = ECKey.fromPublicOnly(spendPrivKey);
|
||||
ECKey tweakPoint = ECKey.fromPublicOnly(ECKey.fromPrivate(tweak));
|
||||
ECKey outputKey = spendPubKey.add(tweakPoint, true);
|
||||
Address address = ScriptType.P2TR.getAddress(PolicyType.SINGLE_SP, outputKey);
|
||||
|
||||
String signature = Bip322.signMessageBip322Sp(address, "Hello World", spendPrivKey, tweak);
|
||||
Assertions.assertTrue(Bip322.verifyMessageBip322(ScriptType.P2TR, address, "Hello World", signature));
|
||||
|
||||
//An SP signature for the same key but a different tweak must not verify against the first address
|
||||
byte[] tweak2 = Utils.hexToBytes("fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210");
|
||||
ECKey tweakPoint2 = ECKey.fromPublicOnly(ECKey.fromPrivate(tweak2));
|
||||
ECKey outputKey2 = spendPubKey.add(tweakPoint2, true);
|
||||
Address address2 = ScriptType.P2TR.getAddress(PolicyType.SINGLE_SP, outputKey2);
|
||||
String signature2 = Bip322.signMessageBip322Sp(address2, "Hello World", spendPrivKey, tweak2);
|
||||
Assertions.assertTrue(Bip322.verifyMessageBip322(ScriptType.P2TR, address2, "Hello World", signature2));
|
||||
Assertions.assertFalse(Bip322.verifyMessageBip322(ScriptType.P2TR, address, "Hello World", signature2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getBip322PsbtSp() {
|
||||
ECKey spendPrivKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
|
||||
byte[] tweak = Utils.hexToBytes("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
|
||||
|
||||
ECKey spendPubKey = ECKey.fromPublicOnly(spendPrivKey);
|
||||
ECKey tweakPoint = ECKey.fromPublicOnly(ECKey.fromPrivate(tweak));
|
||||
ECKey outputKey = spendPubKey.add(tweakPoint, true);
|
||||
Address address = ScriptType.P2TR.getAddress(PolicyType.SINGLE_SP, outputKey);
|
||||
|
||||
PSBT psbt = Bip322.getBip322PsbtSp(address, "Hello World", tweak, java.util.Collections.emptyMap());
|
||||
Assertions.assertEquals(1, psbt.getPsbtInputs().size());
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
|
||||
Assertions.assertNotNull(psbtInput.getWitnessUtxo());
|
||||
Assertions.assertArrayEquals(tweak, psbtInput.getSilentPaymentsTweak());
|
||||
Assertions.assertNull(psbtInput.getTapInternalKey());
|
||||
Assertions.assertTrue(psbtInput.getTapDerivedPublicKeys().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyMessageBip322Multisig() throws SignatureException, InvalidAddressException {
|
||||
Address address = Address.fromString("bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3");
|
||||
|
||||
String message1 = "This will be a p2wsh 3-of-3 multisig BIP 322 signed message";
|
||||
String signature1 = "smpBQBIMEUCIQDQoXvGKLH58exuujBOta+7+GN7vi0lKwiQxzBpuNuXuAIgIE0XYQlFDOfxbegGYYzlf+tqegleAKE6SXYIa1U+uCcBRzBEAiATegywVl6GWrG9jJuPpNwtgHKyVYCX2yfuSSDRFATAaQIgTLlU6reLQsSIrQSF21z3PtUO2yAUseUWGZqRUIE7VKoBSDBFAiEAgxtpidsU0Z4u/+5RB9cyeQtoCW5NcreLJmWXZ8kXCZMCIBR1sXoEinhZE4CF9P9STGIcMvCuZjY6F5F0XTVLj9SjAWlTIQP3dyWvTZjUENWJowMWBsQrrXCUs20Gu5YF79CG5Ga0XSEDwqI5GVBOuFkFzQOGH5eTExSAj2Z/LDV/hbcvAPQdlJMhA17FuuJd+4wGuj+ZbVxEsFapTKAOwyhfw9qpch52JKxbU64=";
|
||||
String signature1 = "BQBIMEUCIQDQoXvGKLH58exuujBOta+7+GN7vi0lKwiQxzBpuNuXuAIgIE0XYQlFDOfxbegGYYzlf+tqegleAKE6SXYIa1U+uCcBRzBEAiATegywVl6GWrG9jJuPpNwtgHKyVYCX2yfuSSDRFATAaQIgTLlU6reLQsSIrQSF21z3PtUO2yAUseUWGZqRUIE7VKoBSDBFAiEAgxtpidsU0Z4u/+5RB9cyeQtoCW5NcreLJmWXZ8kXCZMCIBR1sXoEinhZE4CF9P9STGIcMvCuZjY6F5F0XTVLj9SjAWlTIQP3dyWvTZjUENWJowMWBsQrrXCUs20Gu5YF79CG5Ga0XSEDwqI5GVBOuFkFzQOGH5eTExSAj2Z/LDV/hbcvAPQdlJMhA17FuuJd+4wGuj+ZbVxEsFapTKAOwyhfw9qpch52JKxbU64=";
|
||||
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> Bip322.verifyMessageBip322(ScriptType.P2TR, address, message1, signature1));
|
||||
}
|
||||
|
||||
@ -4,12 +4,8 @@ import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.P2PKHAddress;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.policy.Miniscript;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
@ -298,109 +294,6 @@ public class PSBTTest {
|
||||
Assertions.assertEquals("0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae", Hex.toHexString(psbt1.getPsbtInputs().get(1).getFinalScriptWitness().toByteArray()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void finaliseExternalMultisigInput() throws PSBTParseException {
|
||||
Network.set(Network.REGTEST);
|
||||
String psbtBase64 = "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";
|
||||
|
||||
PSBT psbt = PSBT.fromString(psbtBase64);
|
||||
|
||||
//Input 0: 2-of-3 P2WSH with both required signatures present
|
||||
//Input 1: 1-of-2 P2WSH with the one required signature present
|
||||
Wallet wallet = new Wallet("test") {
|
||||
@Override
|
||||
public Policy getDefaultPolicy() {
|
||||
return new Policy(new Miniscript("")) {
|
||||
@Override
|
||||
public int getNumSignaturesRequired() {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
@Override
|
||||
public java.util.Map<PSBTInput, com.sparrowwallet.drongo.wallet.WalletNode> getSigningNodes(PSBT psbt) {
|
||||
return java.util.Collections.emptyMap();
|
||||
}
|
||||
@Override
|
||||
public java.util.Map<PSBTInput, com.sparrowwallet.drongo.wallet.WalletNode> getSigningNodes(PSBT psbt, boolean useDerivationFallback) {
|
||||
return java.util.Collections.emptyMap();
|
||||
}
|
||||
};
|
||||
wallet.finalise(psbt);
|
||||
|
||||
Assertions.assertTrue(psbt.isFinalized(), "Both inputs should be finalised");
|
||||
|
||||
String expectedInput0Witness = "04004730440220396b5523e3477e2991b6ff6d43c483211ef7dc27971967384a3f7f74be84dbf502201099ca19a5526cdff6557fa42a0def5b9168535c5855afc14c8388c28b7e1f45014730440220202f28a498aaa34e899fe2823a1a44c49d30bda759bacff335a723d0fb16ff0a02205f99caaaf1d39805840f6df85e1adbfcb23d26547191661c78fe619de8ac4313016952210250ca2af1e0f2552436c66078c24736e0396e36938c421fdbee6f4fe377e5f6af2103622f05e03b50037b7de99a00fe6cd1d0950fbb1e1f462cdff85ff077e57e931d2103cc3bf2cb38bfbf03264b567490ed45cd09f465843b0fc7a8dd1be9b852816e0053ae";
|
||||
String expectedInput1Witness = "030047304402203bd2e54c5897addbd269928e713d8e2d54d87ee0c6d0aafb1d035a876090093f02205da2c24f85993a89bb75c4b92df6a849fa5d0cb915cce8d0c4b1889102e1eba7014751210250ca2af1e0f2552436c66078c24736e0396e36938c421fdbee6f4fe377e5f6af2103cc3bf2cb38bfbf03264b567490ed45cd09f465843b0fc7a8dd1be9b852816e0052ae";
|
||||
|
||||
Assertions.assertEquals(expectedInput0Witness, Hex.toHexString(psbt.getPsbtInputs().get(0).getFinalScriptWitness().toByteArray()));
|
||||
Assertions.assertEquals(expectedInput1Witness, Hex.toHexString(psbt.getPsbtInputs().get(1).getFinalScriptWitness().toByteArray()));
|
||||
|
||||
String expectedTxHex = "02000000000102e21d3ab78e6237c1b292cb1445164efd65cc29a412faee98f9f09649eb96ce140000000000fdffffff24856cca96c4454b7084b3a6311caa23423a8b073de77b3e83f449d40f18bf7b0000000000fdffffff03fc310000000000002200206904cd6d7d5300792afd22c26767a7ceb1f28b062d606d9cdad891fdfc65c8fa950f0f0000000000220020b8ea72fcc41bc87d3d6789e36469e04198addf8f278c871bfa3d6b465fefa2aca1410f0000000000220020b1e82ca86dbf4e706a316691c2888e0ac1cfb7f5e65866fc671835d041af2dca04004730440220396b5523e3477e2991b6ff6d43c483211ef7dc27971967384a3f7f74be84dbf502201099ca19a5526cdff6557fa42a0def5b9168535c5855afc14c8388c28b7e1f45014730440220202f28a498aaa34e899fe2823a1a44c49d30bda759bacff335a723d0fb16ff0a02205f99caaaf1d39805840f6df85e1adbfcb23d26547191661c78fe619de8ac4313016952210250ca2af1e0f2552436c66078c24736e0396e36938c421fdbee6f4fe377e5f6af2103622f05e03b50037b7de99a00fe6cd1d0950fbb1e1f462cdff85ff077e57e931d2103cc3bf2cb38bfbf03264b567490ed45cd09f465843b0fc7a8dd1be9b852816e0053ae030047304402203bd2e54c5897addbd269928e713d8e2d54d87ee0c6d0aafb1d035a876090093f02205da2c24f85993a89bb75c4b92df6a849fa5d0cb915cce8d0c4b1889102e1eba7014751210250ca2af1e0f2552436c66078c24736e0396e36938c421fdbee6f4fe377e5f6af2103cc3bf2cb38bfbf03264b567490ed45cd09f465843b0fc7a8dd1be9b852816e0052aeb4000000";
|
||||
Transaction tx = psbt.extractTransaction();
|
||||
Assertions.assertEquals(expectedTxHex, Hex.toHexString(tx.bitcoinSerialize()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void finaliseExternalTaprootKeypathInput() {
|
||||
ECKey spendPrivKey = ECKey.fromPrivate(Utils.hexToBytes("a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"));
|
||||
ECKey tweakKey = ECKey.fromPrivate(Utils.hexToBytes("c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"));
|
||||
byte[] tweak = Utils.bigIntegerToBytes(tweakKey.getPrivKey(), 32);
|
||||
|
||||
ECKey tweakedKey = spendPrivKey.addPrivate(tweakKey);
|
||||
if(tweakedKey.hasOddYCoord()) {
|
||||
tweakedKey = tweakedKey.negatePrivate();
|
||||
}
|
||||
byte[] outputXCoord = tweakedKey.getPubKeyXCoord();
|
||||
|
||||
Transaction prevTx = new Transaction();
|
||||
prevTx.setVersion(2);
|
||||
prevTx.addInput(Sha256Hash.ZERO_HASH, 0, new Script(new byte[0]));
|
||||
prevTx.addOutput(100000L, ScriptType.P2TR.getOutputScript(outputXCoord));
|
||||
|
||||
Transaction spendTx = new Transaction();
|
||||
spendTx.setVersion(2);
|
||||
spendTx.addInput(prevTx.getTxId(), 0, new Script(new byte[0]));
|
||||
spendTx.addOutput(90000L, ScriptType.P2TR.getOutputScript(outputXCoord));
|
||||
|
||||
PSBT psbt = new PSBT(spendTx);
|
||||
psbt.convertVersion(2);
|
||||
psbt.getPsbtInputs().getFirst().setWitnessUtxo(prevTx.getOutputs().getFirst());
|
||||
psbt.getPsbtInputs().getFirst().setSilentPaymentsTweak(tweak);
|
||||
Assertions.assertTrue(psbt.getPsbtInputs().getFirst().signSilentPayments(spendPrivKey));
|
||||
|
||||
TransactionSignature schnorrSig = psbt.getPsbtInputs().getFirst().getTapKeyPathSignature();
|
||||
Assertions.assertNotNull(schnorrSig);
|
||||
|
||||
Wallet wallet = new Wallet("test") {
|
||||
@Override
|
||||
public Policy getDefaultPolicy() {
|
||||
return new Policy(new Miniscript("")) {
|
||||
@Override
|
||||
public int getNumSignaturesRequired() {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
@Override
|
||||
public java.util.Map<PSBTInput, com.sparrowwallet.drongo.wallet.WalletNode> getSigningNodes(PSBT p) {
|
||||
return java.util.Collections.emptyMap();
|
||||
}
|
||||
@Override
|
||||
public java.util.Map<PSBTInput, com.sparrowwallet.drongo.wallet.WalletNode> getSigningNodes(PSBT p, boolean useDerivationFallback) {
|
||||
return java.util.Collections.emptyMap();
|
||||
}
|
||||
};
|
||||
wallet.finalise(psbt);
|
||||
|
||||
Assertions.assertTrue(psbt.isFinalized(), "Taproot keypath input should be finalised");
|
||||
Assertions.assertEquals(0, psbt.getPsbtInputs().getFirst().getFinalScriptSig().getProgram().length);
|
||||
|
||||
TransactionWitness finalWitness = psbt.getPsbtInputs().getFirst().getFinalScriptWitness();
|
||||
Assertions.assertEquals(1, finalWitness.getPushes().size(), "Taproot keypath witness must contain exactly one element (the schnorr signature)");
|
||||
Assertions.assertArrayEquals(schnorrSig.encodeToBitcoin(), finalWitness.getPushes().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializeRoundTrip() throws PSBTParseException {
|
||||
String psbtStr1 = "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQjaBABHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwFHMEQCIGX0W6WZi1mif/4ae+0BavHx+Q1Us6qPdFCqX1aiUQO9AiB/ckcDrR7blmgLKEtW1P/LiPf7dZ6rvgiqMPKbhROD0gFHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4AIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA==";
|
||||
@ -1140,204 +1033,6 @@ public class PSBTTest {
|
||||
Assertions.assertNull(input.getSilentPaymentsTweak());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sighashSingleLegacyMagicOneBugRefusedAtSignTime() {
|
||||
ECKey victimKey = new ECKey();
|
||||
P2PKHAddress victimAddr = new P2PKHAddress(victimKey.getPubKeyHash());
|
||||
Script victimSpk = victimAddr.getOutputScript();
|
||||
|
||||
Transaction priorTx0 = new Transaction();
|
||||
priorTx0.addInput(Sha256Hash.ZERO_HASH, 0L, new Script(new byte[0]));
|
||||
priorTx0.addOutput(100_000L, victimSpk);
|
||||
|
||||
Transaction priorTx1 = new Transaction();
|
||||
priorTx1.addInput(Sha256Hash.ZERO_HASH, 0L, new Script(new byte[0]));
|
||||
priorTx1.addOutput(200_000L, victimSpk);
|
||||
|
||||
Transaction unsigned = new Transaction();
|
||||
unsigned.addInput(priorTx0.getTxId(), 0, new Script(new byte[0]));
|
||||
unsigned.addInput(priorTx1.getTxId(), 0, new Script(new byte[0]));
|
||||
unsigned.addOutput(90_000L, victimSpk);
|
||||
|
||||
PSBT psbt = new PSBT(unsigned);
|
||||
PSBTInput trapInput = psbt.getPsbtInputs().get(1);
|
||||
trapInput.setNonWitnessUtxo(priorTx1);
|
||||
trapInput.setSigHash(SigHash.SINGLE);
|
||||
|
||||
IllegalStateException thrown = Assertions.assertThrows(IllegalStateException.class, () -> trapInput.sign(victimKey));
|
||||
Assertions.assertTrue(thrown.getMessage().contains("re-broadcastable"), "guard message should describe the risk");
|
||||
|
||||
Sha256Hash magicOne = Sha256Hash.wrap("0100000000000000000000000000000000000000000000000000000000000000");
|
||||
Assertions.assertEquals(magicOne, unsigned.hashForLegacySignature(1, victimSpk, SigHash.SINGLE),
|
||||
"underlying legacy hash function still returns the constant - guard is the sole defence at sign time");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifySigHashesAcceptsSafeSighashes() throws PSBTSignatureException {
|
||||
ECKey key = new ECKey();
|
||||
Script spk = new P2PKHAddress(key.getPubKeyHash()).getOutputScript();
|
||||
Transaction prior = new Transaction();
|
||||
prior.addInput(Sha256Hash.ZERO_HASH, 0L, new Script(new byte[0]));
|
||||
prior.addOutput(100_000L, spk);
|
||||
|
||||
Transaction unsigned = new Transaction();
|
||||
unsigned.addInput(prior.getTxId(), 0, new Script(new byte[0]));
|
||||
unsigned.addOutput(90_000L, spk);
|
||||
|
||||
PSBT psbt = new PSBT(unsigned);
|
||||
psbt.getPsbtInputs().get(0).setNonWitnessUtxo(prior);
|
||||
|
||||
// unset sighash, ALL, and DEFAULT should all pass
|
||||
psbt.verifySigHashes();
|
||||
psbt.getPsbtInputs().get(0).setSigHash(SigHash.ALL);
|
||||
psbt.verifySigHashes();
|
||||
psbt.getPsbtInputs().get(0).setSigHash(SigHash.DEFAULT);
|
||||
psbt.verifySigHashes();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifySigHashesReportsMostSeriousIssue() {
|
||||
ECKey key = new ECKey();
|
||||
Script spk = new P2PKHAddress(key.getPubKeyHash()).getOutputScript();
|
||||
Transaction prior0 = new Transaction();
|
||||
prior0.addInput(Sha256Hash.ZERO_HASH, 0L, new Script(new byte[0]));
|
||||
prior0.addOutput(100_000L, spk);
|
||||
Transaction prior1 = new Transaction();
|
||||
prior1.addInput(Sha256Hash.ZERO_HASH, 0L, new Script(new byte[0]));
|
||||
prior1.addOutput(200_000L, spk);
|
||||
|
||||
Transaction unsigned = new Transaction();
|
||||
unsigned.addInput(prior0.getTxId(), 0, new Script(new byte[0]));
|
||||
unsigned.addInput(prior1.getTxId(), 0, new Script(new byte[0]));
|
||||
unsigned.addOutput(45_000L, spk);
|
||||
unsigned.addOutput(45_000L, spk);
|
||||
|
||||
PSBT psbt = new PSBT(unsigned);
|
||||
psbt.getPsbtInputs().get(0).setNonWitnessUtxo(prior0);
|
||||
psbt.getPsbtInputs().get(0).setSigHash(SigHash.SINGLE); // severity 3
|
||||
psbt.getPsbtInputs().get(1).setNonWitnessUtxo(prior1);
|
||||
psbt.getPsbtInputs().get(1).setSigHash(SigHash.NONE); // severity 5
|
||||
|
||||
PSBTSignatureException ex = Assertions.assertThrows(PSBTSignatureException.class, psbt::verifySigHashes);
|
||||
Assertions.assertTrue(ex.getMessage().contains("SIGHASH_NONE"), "should report the higher-severity NONE input");
|
||||
Assertions.assertTrue(ex.getMessage().contains("Input 1"), "should identify the offending input index");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyCombinedSignaturesRejectsSigHashTampering() {
|
||||
ECKey key = new ECKey();
|
||||
Script spk = new P2PKHAddress(key.getPubKeyHash()).getOutputScript();
|
||||
Transaction prior0 = new Transaction();
|
||||
prior0.addInput(Sha256Hash.ZERO_HASH, 0L, new Script(new byte[0]));
|
||||
prior0.addOutput(100_000L, spk);
|
||||
Transaction prior1 = new Transaction();
|
||||
prior1.addInput(Sha256Hash.ZERO_HASH, 0L, new Script(new byte[0]));
|
||||
prior1.addOutput(200_000L, spk);
|
||||
|
||||
Transaction tx = new Transaction();
|
||||
tx.addInput(prior0.getTxId(), 0, new Script(new byte[0]));
|
||||
tx.addInput(prior1.getTxId(), 0, new Script(new byte[0]));
|
||||
tx.addOutput(90_000L, spk);
|
||||
|
||||
PSBT localPsbt = new PSBT(tx);
|
||||
localPsbt.getPsbtInputs().get(0).setNonWitnessUtxo(prior0);
|
||||
localPsbt.getPsbtInputs().get(0).setSigHash(SigHash.ALL);
|
||||
localPsbt.getPsbtInputs().get(1).setNonWitnessUtxo(prior1);
|
||||
localPsbt.getPsbtInputs().get(1).setSigHash(SigHash.ALL);
|
||||
|
||||
PSBT tamperedPsbt = new PSBT(tx);
|
||||
tamperedPsbt.getPsbtInputs().get(0).setNonWitnessUtxo(prior0);
|
||||
tamperedPsbt.getPsbtInputs().get(0).setSigHash(SigHash.ALL);
|
||||
tamperedPsbt.getPsbtInputs().get(1).setNonWitnessUtxo(prior1);
|
||||
tamperedPsbt.getPsbtInputs().get(1).setSigHash(SigHash.SINGLE);
|
||||
|
||||
SigHash originalLocalSigHash = localPsbt.getPsbtInputs().get(1).getSigHash();
|
||||
Assertions.assertEquals(SigHash.ALL, originalLocalSigHash);
|
||||
|
||||
PSBTSignatureException ex = Assertions.assertThrows(PSBTSignatureException.class,
|
||||
() -> localPsbt.verifyCombinedSignatures(tamperedPsbt));
|
||||
Assertions.assertTrue(ex.getMessage().contains("Combined PSBT would change sighash"),
|
||||
"should describe the combine-time sighash change");
|
||||
Assertions.assertTrue(ex.getMessage().contains("Input 1"),
|
||||
"should identify which input the combine would tamper with");
|
||||
Assertions.assertTrue(ex.getMessage().contains("SIGHASH_SINGLE"),
|
||||
"should describe the unsafe sighash being introduced");
|
||||
|
||||
Assertions.assertEquals(originalLocalSigHash, localPsbt.getPsbtInputs().get(1).getSigHash(),
|
||||
"local PSBT must be unchanged when verifyCombinedSignatures rejects the combine");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyCombinedSignaturesAcceptsUnchangedUnsafeSigHash() throws PSBTSignatureException {
|
||||
ECKey key = new ECKey();
|
||||
Script spk = new P2PKHAddress(key.getPubKeyHash()).getOutputScript();
|
||||
Transaction prior = new Transaction();
|
||||
prior.addInput(Sha256Hash.ZERO_HASH, 0L, new Script(new byte[0]));
|
||||
prior.addOutput(100_000L, spk);
|
||||
|
||||
Transaction tx = new Transaction();
|
||||
tx.addInput(prior.getTxId(), 0, new Script(new byte[0]));
|
||||
tx.addOutput(90_000L, spk);
|
||||
|
||||
PSBT localPsbt = new PSBT(tx);
|
||||
localPsbt.getPsbtInputs().get(0).setNonWitnessUtxo(prior);
|
||||
localPsbt.getPsbtInputs().get(0).setSigHash(SigHash.SINGLE);
|
||||
|
||||
PSBT incomingPsbt = new PSBT(tx);
|
||||
incomingPsbt.getPsbtInputs().get(0).setNonWitnessUtxo(prior);
|
||||
incomingPsbt.getPsbtInputs().get(0).setSigHash(SigHash.SINGLE);
|
||||
|
||||
localPsbt.verifyCombinedSignatures(incomingPsbt);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyCombinedSignaturesRejectsAdditiveUnsafeSigHash() {
|
||||
ECKey key = new ECKey();
|
||||
Script spk = new P2PKHAddress(key.getPubKeyHash()).getOutputScript();
|
||||
Transaction prior = new Transaction();
|
||||
prior.addInput(Sha256Hash.ZERO_HASH, 0L, new Script(new byte[0]));
|
||||
prior.addOutput(100_000L, spk);
|
||||
|
||||
Transaction tx = new Transaction();
|
||||
tx.addInput(prior.getTxId(), 0, new Script(new byte[0]));
|
||||
tx.addOutput(90_000L, spk);
|
||||
|
||||
PSBT localPsbt = new PSBT(tx);
|
||||
localPsbt.getPsbtInputs().get(0).setNonWitnessUtxo(prior);
|
||||
localPsbt.getPsbtInputs().get(0).setSigHash(null);
|
||||
|
||||
PSBT incomingPsbt = new PSBT(tx);
|
||||
incomingPsbt.getPsbtInputs().get(0).setNonWitnessUtxo(prior);
|
||||
incomingPsbt.getPsbtInputs().get(0).setSigHash(SigHash.NONE);
|
||||
|
||||
PSBTSignatureException ex = Assertions.assertThrows(PSBTSignatureException.class,
|
||||
() -> localPsbt.verifyCombinedSignatures(incomingPsbt));
|
||||
Assertions.assertTrue(ex.getMessage().contains("Combined PSBT would change sighash"));
|
||||
Assertions.assertTrue(ex.getMessage().contains("Input 0"));
|
||||
Assertions.assertTrue(ex.getMessage().contains("SIGHASH_NONE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifySigHashesRejectsPlainSighashSingle() {
|
||||
ECKey key = new ECKey();
|
||||
Script spk = new P2PKHAddress(key.getPubKeyHash()).getOutputScript();
|
||||
Transaction prior = new Transaction();
|
||||
prior.addInput(Sha256Hash.ZERO_HASH, 0L, new Script(new byte[0]));
|
||||
prior.addOutput(100_000L, spk);
|
||||
|
||||
Transaction unsigned = new Transaction();
|
||||
unsigned.addInput(prior.getTxId(), 0, new Script(new byte[0]));
|
||||
unsigned.addOutput(45_000L, spk);
|
||||
unsigned.addOutput(45_000L, spk);
|
||||
|
||||
PSBT psbt = new PSBT(unsigned);
|
||||
psbt.getPsbtInputs().get(0).setNonWitnessUtxo(prior);
|
||||
psbt.getPsbtInputs().get(0).setSigHash(SigHash.SINGLE);
|
||||
|
||||
PSBTSignatureException ex = Assertions.assertThrows(PSBTSignatureException.class, psbt::verifySigHashes);
|
||||
Assertions.assertTrue(ex.getMessage().contains("SIGHASH_SINGLE"));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
Network.set(null);
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package com.sparrowwallet.drongo.silentpayments;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.P2TRAddress;
|
||||
@ -8,8 +7,6 @@ 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.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@ -17,7 +14,6 @@ import org.junit.jupiter.api.Test;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
|
||||
public class SilentPaymentUtilsTest {
|
||||
@ -995,194 +991,4 @@ public class SilentPaymentUtilsTest {
|
||||
|
||||
Assertions.assertTrue(matches.isEmpty());
|
||||
}
|
||||
|
||||
private static final String VERIFY_TEST_SEED = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor";
|
||||
private static final String VERIFY_TEST_SP_ADDRESS = "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv";
|
||||
private static final long VERIFY_INPUT_VALUE = 100_000L;
|
||||
private static final long VERIFY_OUTPUT_VALUE = 50_000L;
|
||||
|
||||
@Test
|
||||
public void testVerifyValidPsbt() throws Exception {
|
||||
Wallet wallet = buildVerifyWallet();
|
||||
WalletNode receiveNode = primeReceiveNode(wallet, 0);
|
||||
PSBT psbt = buildVerifySendingPsbt(receiveNode);
|
||||
|
||||
Map<PSBTInput, WalletNode> signingNodes = wallet.getSigningNodes(psbt);
|
||||
Assertions.assertEquals(1, signingNodes.size(), "Wallet should recognize its own input");
|
||||
wallet.computeSilentPaymentOutputs(psbt, signingNodes);
|
||||
|
||||
Map<Address, SilentPaymentAddress> verified = wallet.verifySilentPaymentOutputs(psbt);
|
||||
Assertions.assertEquals(1, verified.size(), "A legitimately constructed SP PSBT must verify");
|
||||
Assertions.assertEquals(SilentPaymentAddress.from(VERIFY_TEST_SP_ADDRESS), verified.values().iterator().next());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVerifyRejectsNoProof() throws Exception {
|
||||
Wallet wallet = buildVerifyWallet();
|
||||
WalletNode receiveNode = primeReceiveNode(wallet, 0);
|
||||
PSBT psbt = buildVerifySendingPsbt(receiveNode);
|
||||
|
||||
byte[] arbitraryXOnly = new byte[32];
|
||||
new SecureRandom().nextBytes(arbitraryXOnly);
|
||||
psbt.getPsbtOutputs().get(0).setScript(ScriptType.P2TR.getOutputScript(arbitraryXOnly));
|
||||
|
||||
Map<Address, SilentPaymentAddress> verified = wallet.verifySilentPaymentOutputs(psbt);
|
||||
Assertions.assertTrue(verified.isEmpty(), "Poisoned PSBT without BIP-375 metadata must be rejected");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVerifyRejectsForgedShare() throws Exception {
|
||||
Wallet wallet = buildVerifyWallet();
|
||||
WalletNode receiveNode = primeReceiveNode(wallet, 0);
|
||||
PSBT psbt = buildVerifySendingPsbt(receiveNode);
|
||||
|
||||
Map<PSBTInput, WalletNode> signingNodes = wallet.getSigningNodes(psbt);
|
||||
wallet.computeSilentPaymentOutputs(psbt, signingNodes);
|
||||
|
||||
Map.Entry<ECKey, ECKey> shareEntry = psbt.getSilentPaymentsEcdhShares().entrySet().iterator().next();
|
||||
ECKey forged = ECKey.fromPublicOnly(ECKey.fromPrivate(Sha256Hash.hash("forged".getBytes())).getPubKey());
|
||||
psbt.getSilentPaymentsEcdhShares().put(shareEntry.getKey(), forged);
|
||||
|
||||
Map<Address, SilentPaymentAddress> verified = wallet.verifySilentPaymentOutputs(psbt);
|
||||
Assertions.assertTrue(verified.isEmpty(), "Forged ECDH share must fail DLEQ verification");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVerifyRejectsMutatedScript() throws Exception {
|
||||
Wallet wallet = buildVerifyWallet();
|
||||
WalletNode receiveNode = primeReceiveNode(wallet, 0);
|
||||
PSBT psbt = buildVerifySendingPsbt(receiveNode);
|
||||
|
||||
Map<PSBTInput, WalletNode> signingNodes = wallet.getSigningNodes(psbt);
|
||||
wallet.computeSilentPaymentOutputs(psbt, signingNodes);
|
||||
|
||||
byte[] mutatedXOnly = new byte[32];
|
||||
new SecureRandom().nextBytes(mutatedXOnly);
|
||||
psbt.getPsbtOutputs().get(0).setScript(ScriptType.P2TR.getOutputScript(mutatedXOnly));
|
||||
|
||||
Map<Address, SilentPaymentAddress> verified = wallet.verifySilentPaymentOutputs(psbt);
|
||||
Assertions.assertTrue(verified.isEmpty(), "Output script that doesn't derive from the claimed SP address must be rejected");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVerifyAbortsMidSign() throws Exception {
|
||||
Wallet wallet = buildVerifyWallet();
|
||||
WalletNode receiveNode = primeReceiveNode(wallet, 0);
|
||||
PSBT psbt = buildVerifySendingPsbt(receiveNode);
|
||||
|
||||
Map<Address, SilentPaymentAddress> verified = wallet.verifySilentPaymentOutputs(psbt);
|
||||
Assertions.assertTrue(verified.isEmpty(), "Mid-sign PSBT must abort all-or-nothing without persisting");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVerifyRejectsForeignInput() throws Exception {
|
||||
Wallet wallet = buildVerifyWallet();
|
||||
WalletNode receiveNode = primeReceiveNode(wallet, 0);
|
||||
PSBT psbt = buildVerifySendingPsbt(receiveNode);
|
||||
|
||||
byte[] foreignXOnly = new byte[32];
|
||||
new SecureRandom().nextBytes(foreignXOnly);
|
||||
Script foreignScript = ScriptType.P2TR.getOutputScript(foreignXOnly);
|
||||
psbt.getPsbtInputs().get(0).setWitnessUtxo(new TransactionOutput(null, VERIFY_INPUT_VALUE, foreignScript));
|
||||
|
||||
Map<Address, SilentPaymentAddress> verified = wallet.verifySilentPaymentOutputs(psbt);
|
||||
Assertions.assertTrue(verified.isEmpty(), "PSBT whose inputs aren't from this wallet must be rejected");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVerifyEmptyPsbt() throws Exception {
|
||||
Wallet wallet = buildVerifyWallet();
|
||||
primeReceiveNode(wallet, 0);
|
||||
|
||||
Transaction tx = new Transaction();
|
||||
tx.setVersion(2);
|
||||
PSBT psbt = new PSBT(tx);
|
||||
|
||||
Map<Address, SilentPaymentAddress> verified = wallet.verifySilentPaymentOutputs(psbt);
|
||||
Assertions.assertTrue(verified.isEmpty(), "PSBT with no SP outputs returns empty by definition");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVerifyRejectsMissingProof() throws Exception {
|
||||
Wallet wallet = buildVerifyWallet();
|
||||
WalletNode receiveNode = primeReceiveNode(wallet, 0);
|
||||
PSBT psbt = buildVerifySendingPsbt(receiveNode);
|
||||
|
||||
Map<PSBTInput, WalletNode> signingNodes = wallet.getSigningNodes(psbt);
|
||||
wallet.computeSilentPaymentOutputs(psbt, signingNodes);
|
||||
|
||||
psbt.getSilentPaymentsDLEQProofs().clear();
|
||||
|
||||
Map<Address, SilentPaymentAddress> verified = wallet.verifySilentPaymentOutputs(psbt);
|
||||
Assertions.assertTrue(verified.isEmpty(), "Missing DLEQ proof must cause rejection");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testComputeRejectsInjectedSpInfo() throws Exception {
|
||||
Wallet wallet = buildVerifyWallet();
|
||||
WalletNode receiveNode = primeReceiveNode(wallet, 0);
|
||||
PSBT psbt = buildVerifySendingPsbt(receiveNode);
|
||||
|
||||
byte[] baitXOnly = new byte[32];
|
||||
new SecureRandom().nextBytes(baitXOnly);
|
||||
Script baitScript = ScriptType.P2TR.getOutputScript(baitXOnly);
|
||||
psbt.getPsbtOutputs().getFirst().setScript(baitScript);
|
||||
|
||||
Map<PSBTInput, WalletNode> signingNodes = wallet.getSigningNodes(psbt);
|
||||
Assertions.assertThrows(InvalidSilentPaymentException.class, () -> wallet.computeSilentPaymentOutputs(psbt, signingNodes),
|
||||
"Injected PSBT_OUT_SP_V0_INFO without valid BIP-375 proofs must abort signing");
|
||||
Assertions.assertArrayEquals(baitScript.getProgram(), psbt.getPsbtOutputs().get(0).getScript().getProgram(),
|
||||
"Visible output script must not be mutated when SP metadata fails verification");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testComputePreservesScriptForVerifiedPsbt() throws Exception {
|
||||
Wallet wallet = buildVerifyWallet();
|
||||
WalletNode receiveNode = primeReceiveNode(wallet, 0);
|
||||
PSBT psbt = buildVerifySendingPsbt(receiveNode);
|
||||
|
||||
Map<PSBTInput, WalletNode> signingNodes = wallet.getSigningNodes(psbt);
|
||||
wallet.computeSilentPaymentOutputs(psbt, signingNodes);
|
||||
byte[] computedScript = psbt.getPsbtOutputs().getFirst().getScript().getProgram();
|
||||
|
||||
wallet.computeSilentPaymentOutputs(psbt, signingNodes);
|
||||
Assertions.assertArrayEquals(computedScript, psbt.getPsbtOutputs().getFirst().getScript().getProgram(),
|
||||
"Already-computed and proof-backed SP output script must be preserved by a subsequent compute call");
|
||||
}
|
||||
|
||||
private static Wallet buildVerifyWallet() throws Exception {
|
||||
DeterministicSeed seed = new DeterministicSeed(VERIFY_TEST_SEED, "", 0, DeterministicSeed.Type.BIP39);
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setPolicyType(PolicyType.SINGLE_HD);
|
||||
wallet.setScriptType(ScriptType.P2WPKH);
|
||||
Keystore keystore = Keystore.fromSeed(seed, PolicyType.SINGLE_HD, ScriptType.P2WPKH.getDefaultDerivation());
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, ScriptType.P2WPKH, wallet.getKeystores(), 1));
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
private static WalletNode primeReceiveNode(Wallet wallet, int index) {
|
||||
WalletNode node = new WalletNode(wallet, KeyPurpose.RECEIVE, index);
|
||||
TreeSet<WalletNode> children = new TreeSet<>();
|
||||
children.add(node);
|
||||
wallet.getNode(KeyPurpose.RECEIVE).setChildren(children);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private static PSBT buildVerifySendingPsbt(WalletNode receiveNode) {
|
||||
Script inputScript = receiveNode.getOutputScript();
|
||||
|
||||
Transaction tx = new Transaction();
|
||||
tx.setVersion(2);
|
||||
tx.addInput(Sha256Hash.wrap("0000000000000000000000000000000000000000000000000000000000000001"), 0, new Script(new byte[0]));
|
||||
tx.addOutput(VERIFY_OUTPUT_VALUE, new Script(new byte[0]));
|
||||
|
||||
PSBT psbt = new PSBT(tx);
|
||||
psbt.getPsbtInputs().get(0).setWitnessUtxo(new TransactionOutput(null, VERIFY_INPUT_VALUE, inputScript));
|
||||
psbt.getPsbtOutputs().get(0).setSilentPaymentAddress(SilentPaymentAddress.from(VERIFY_TEST_SP_ADDRESS));
|
||||
|
||||
return psbt;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,13 +3,9 @@ package com.sparrowwallet.drongo.uri;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
|
||||
public class BitcoinUriTest {
|
||||
private static final String ADDRESS = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4";
|
||||
|
||||
@Test
|
||||
public void testSamourai() throws BitcoinURIParseException {
|
||||
String uri = "bitcoin:BC1QT4NRM47695YWDG9N30N68JARMXRJNKFMR36994?amount=0,001";
|
||||
@ -18,34 +14,4 @@ public class BitcoinUriTest {
|
||||
Assertions.assertEquals("BC1QT4NRM47695YWDG9N30N68JARMXRJNKFMR36994".toLowerCase(Locale.ROOT), bitcoinURI.getAddress().toString());
|
||||
Assertions.assertEquals(Long.valueOf(100000), bitcoinURI.getAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptsHttpsPayjoinUrl() throws BitcoinURIParseException {
|
||||
BitcoinURI bitcoinURI = payjoinUri("https://example.com/payjoin");
|
||||
Assertions.assertNotNull(bitcoinURI.getPayjoinUrl());
|
||||
Assertions.assertEquals("https://example.com/payjoin", bitcoinURI.getPayjoinUrl().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptsHttpOnionPayjoinUrl() throws BitcoinURIParseException {
|
||||
BitcoinURI bitcoinURI = payjoinUri("http://abcdefghijklmnopqrstuvwxyzabcdefghijklmnop.onion/payjoin");
|
||||
Assertions.assertNotNull(bitcoinURI.getPayjoinUrl());
|
||||
Assertions.assertEquals("http://abcdefghijklmnopqrstuvwxyzabcdefghijklmnop.onion/payjoin", bitcoinURI.getPayjoinUrl().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsNonHttpOnionPayjoinUrl() throws BitcoinURIParseException {
|
||||
BitcoinURI bitcoinURI = payjoinUri("file://abcdefghijklmnopqrstuvwxyzabcdefghijklmnop.onion/payjoin");
|
||||
Assertions.assertNull(bitcoinURI.getPayjoinUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsMalformedPayjoinUrl() throws BitcoinURIParseException {
|
||||
BitcoinURI bitcoinURI = payjoinUri("payjoin");
|
||||
Assertions.assertNull(bitcoinURI.getPayjoinUrl());
|
||||
}
|
||||
|
||||
private static BitcoinURI payjoinUri(String payjoinUrl) throws BitcoinURIParseException {
|
||||
return new BitcoinURI("bitcoin:" + ADDRESS + "?pj=" + URLEncoder.encode(payjoinUrl, StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user