implement bip375 with tests
This commit is contained in:
parent
8e134305c5
commit
6bd3d80303
@ -8,6 +8,8 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class KeyDerivation {
|
||||
public static final String DEFAULT_WATCH_ONLY_FINGERPRINT = "00000000";
|
||||
|
||||
private final String masterFingerprint;
|
||||
private final String derivationPath;
|
||||
private transient List<ChildNumber> derivation;
|
||||
|
||||
@ -5,14 +5,18 @@ import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.silentpayments.InvalidSilentPaymentException;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentUtils;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentsDLEQProof;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.drongo.psbt.PSBTEntry.*;
|
||||
import static com.sparrowwallet.drongo.psbt.PSBTInput.*;
|
||||
@ -27,6 +31,8 @@ public class PSBT {
|
||||
public static final byte PSBT_GLOBAL_INPUT_COUNT = 0x04;
|
||||
public static final byte PSBT_GLOBAL_OUTPUT_COUNT = 0x05;
|
||||
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_VERSION = (byte)0xfb;
|
||||
public static final byte PSBT_GLOBAL_PROPRIETARY = (byte)0xfc;
|
||||
|
||||
@ -43,17 +49,21 @@ public class PSBT {
|
||||
|
||||
private byte[] psbtBytes;
|
||||
|
||||
private Transaction transaction = null;
|
||||
private Integer version = null;
|
||||
private final Map<ExtendedKey, KeyDerivation> extendedPublicKeys = new LinkedHashMap<>();
|
||||
private final Map<String, String> globalProprietary = new LinkedHashMap<>();
|
||||
|
||||
//PSBTv2 fields
|
||||
//PSBTv0-only fields
|
||||
private Transaction transaction = null;
|
||||
|
||||
//PSBTv2-only fields
|
||||
private Long txVersion = null;
|
||||
private Long fallbackLocktime = null;
|
||||
private Long inputCount = null;
|
||||
private Long outputCount = null;
|
||||
private Byte modifiable = null;
|
||||
private final Map<ECKey, ECKey> silentPaymentsEcdhShares = new LinkedHashMap<>();
|
||||
private final Map<ECKey, SilentPaymentsDLEQProof> silentPaymentsDLEQProofs = new LinkedHashMap<>();
|
||||
|
||||
private final List<PSBTInput> psbtInputs = new ArrayList<>();
|
||||
private final List<PSBTOutput> psbtOutputs = new ArrayList<>();
|
||||
@ -326,12 +336,18 @@ public class PSBT {
|
||||
break;
|
||||
case PSBT_GLOBAL_TX_VERSION:
|
||||
entry.checkOneByteKey();
|
||||
if(entry.getData().length != 4) {
|
||||
throw new PSBTParseException("PSBT global tx version must be 4 bytes");
|
||||
}
|
||||
long txVersion = Utils.readUint32(entry.getData(), 0);
|
||||
this.txVersion = txVersion;
|
||||
log.debug("PSBT tx version: " + txVersion);
|
||||
break;
|
||||
case PSBT_GLOBAL_FALLBACK_LOCKTIME:
|
||||
entry.checkOneByteKey();
|
||||
if(entry.getData().length != 4) {
|
||||
throw new PSBTParseException("PSBT global fallback locktime must be 4 bytes");
|
||||
}
|
||||
long fallbackLocktime = Utils.readUint32(entry.getData(), 0);
|
||||
this.fallbackLocktime = fallbackLocktime;
|
||||
log.debug("PSBT fallback locktime: " + fallbackLocktime);
|
||||
@ -360,10 +376,33 @@ public class PSBT {
|
||||
break;
|
||||
case PSBT_GLOBAL_VERSION:
|
||||
entry.checkOneByteKey();
|
||||
if(entry.getData().length != 4) {
|
||||
throw new PSBTParseException("PSBT global version must be 4 bytes");
|
||||
}
|
||||
int version = (int)Utils.readUint32(entry.getData(), 0);
|
||||
this.version = version;
|
||||
log.debug("PSBT version: " + version);
|
||||
break;
|
||||
case PSBT_GLOBAL_SP_ECDH_SHARE:
|
||||
entry.checkOneBytePlusPubKey();
|
||||
if(entry.getData().length != 33) {
|
||||
throw new PSBTParseException("PSBT global silent payments ECDH share data must be 33 bytes");
|
||||
}
|
||||
ECKey scanKey = ECKey.fromPublicOnly(entry.getKeyData());
|
||||
ECKey ecdhShare = ECKey.fromPublicOnly(entry.getData());
|
||||
this.silentPaymentsEcdhShares.put(scanKey, ecdhShare);
|
||||
log.debug("PSBT global silent payments ECDH share for scan key: " + Utils.bytesToHex(entry.getKeyData()));
|
||||
break;
|
||||
case PSBT_GLOBAL_SP_DLEQ:
|
||||
entry.checkOneBytePlusPubKey();
|
||||
if(entry.getData().length != 64) {
|
||||
throw new PSBTParseException("PSBT global silent payments DLEQ proof data must be 64 bytes");
|
||||
}
|
||||
ECKey proofScanKey = ECKey.fromPublicOnly(entry.getKeyData());
|
||||
SilentPaymentsDLEQProof dleqProof = SilentPaymentsDLEQProof.fromBytes(entry.getData());
|
||||
this.silentPaymentsDLEQProofs.put(proofScanKey, dleqProof);
|
||||
log.debug("PSBT global silent payments DLEQ proof for scan key: " + Utils.bytesToHex(entry.getKeyData()));
|
||||
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()));
|
||||
@ -392,6 +431,12 @@ public class PSBT {
|
||||
if(modifiable != null) {
|
||||
throw new PSBTParseException("PSBT_GLOBAL_TX_MODIFIABLE is not allowed in PSBTv0");
|
||||
}
|
||||
if(!silentPaymentsEcdhShares.isEmpty()) {
|
||||
throw new PSBTParseException("PSBT_GLOBAL_SP_ECDH_SHARE is not allowed in PSBTv0");
|
||||
}
|
||||
if(!silentPaymentsDLEQProofs.isEmpty()) {
|
||||
throw new PSBTParseException("PSBT_GLOBAL_SP_DLEQ is not allowed in PSBTv0");
|
||||
}
|
||||
} else if(getPsbtVersion() == 1) {
|
||||
throw new PSBTParseException("There is no PSBTv1");
|
||||
} else if(getPsbtVersion() >= 2) {
|
||||
@ -436,6 +481,12 @@ public class PSBT {
|
||||
if(input.getRequiredHeightLocktime() != null) {
|
||||
throw new PSBTParseException("PSBT_IN_REQUIRED_HEIGHT_LOCKTIME is not allowed in PSBTv0");
|
||||
}
|
||||
if(!input.getSilentPaymentsEcdhShares().isEmpty()) {
|
||||
throw new PSBTParseException("PSBT_IN_SP_ECDH_SHARE is not allowed in PSBTv0");
|
||||
}
|
||||
if(!input.getSilentPaymentsDLEQProofs().isEmpty()) {
|
||||
throw new PSBTParseException("PSBT_IN_SP_DLEQ is not allowed in PSBTv0");
|
||||
}
|
||||
} else if(getPsbtVersion() >= 2) {
|
||||
if(input.prevTxid() == null) {
|
||||
throw new PSBTParseException("PSBT_IN_PREV_TXID is required in PSBTv2");
|
||||
@ -466,6 +517,12 @@ public class PSBT {
|
||||
if(output.script() != null) {
|
||||
throw new PSBTParseException("PSBT_OUT_SCRIPT is not allowed in PSBTv0");
|
||||
}
|
||||
if(output.getSilentPaymentAddress() != null) {
|
||||
throw new PSBTParseException("PSBT_OUT_SP_V0_INFO is not allowed in PSBTv0");
|
||||
}
|
||||
if(output.getSilentPaymentLabel() != null) {
|
||||
throw new PSBTParseException("PSBT_OUT_SP_V0_LABEL is not allowed in PSBTv0");
|
||||
}
|
||||
} else if(getPsbtVersion() >= 2) {
|
||||
if(output.amount() == null) {
|
||||
throw new PSBTParseException("PSBT_OUT_AMOUNT is required in PSBTv2");
|
||||
@ -531,6 +588,99 @@ public class PSBT {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates silent payment ECDH shares and DLEQ proofs according to BIP-375.
|
||||
*
|
||||
* For each silent payment output, validates that:
|
||||
* 1. Either global or per-input ECDH shares and DLEQ proofs are provided for Taproot inputs
|
||||
* 2. The DLEQ proofs are cryptographically valid
|
||||
* 3. The output scripts match the expected scripts computed from the ECDH shares
|
||||
*
|
||||
* @param inputPublicKeys Map of PSBTInput to their public keys for verification
|
||||
* @throws PSBTProofException if validation fails
|
||||
*/
|
||||
public void validateSilentPayments(Map<TransactionInput, ECKey> inputPublicKeys) throws PSBTProofException {
|
||||
Set<ECKey> scanKeys = getPsbtOutputs().stream().filter(output -> output.getSilentPaymentAddress() != null)
|
||||
.map(output -> output.getSilentPaymentAddress().getScanKey()).collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
if(scanKeys.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(PSBTInput input : getPsbtInputs()) {
|
||||
SigHash sigHash = input.getSigHash();
|
||||
if(sigHash != null && sigHash != SigHash.ALL && sigHash != SigHash.DEFAULT) {
|
||||
throw new PSBTProofException("Silent payment outputs require SIGHASH_ALL signatures only. Input at index " + input.getIndex() + " has sighash type: " + sigHash);
|
||||
}
|
||||
}
|
||||
|
||||
Set<HashIndex> outpoints = inputPublicKeys.keySet().stream()
|
||||
.map(input -> new HashIndex(input.getOutpoint().getHash(), input.getOutpoint().getIndex()))
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
ECKey summedPublicKey = SilentPaymentUtils.getSummedPublicKey(inputPublicKeys.values());
|
||||
if(summedPublicKey == null) {
|
||||
throw new PSBTProofException("Cannot verify global DLEQ proof without input public keys");
|
||||
}
|
||||
|
||||
for(ECKey scanKey : scanKeys) {
|
||||
List<SilentPayment> silentPayments = getPsbtOutputs().stream()
|
||||
.filter(output -> output.getSilentPaymentAddress() != null && output.getSilentPaymentAddress().getScanKey().equals(scanKey) && output.getScript() != null)
|
||||
.map(psbtOutput -> new SilentPayment(psbtOutput.getSilentPaymentAddress(), psbtOutput.getScript().getToAddress(), null, psbtOutput.getAmount(), false))
|
||||
.collect(Collectors.toList());
|
||||
if(silentPayments.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean hasGlobalEcdhShare = silentPaymentsEcdhShares.containsKey(scanKey);
|
||||
boolean hasGlobalDleqProof = silentPaymentsDLEQProofs.containsKey(scanKey);
|
||||
if(hasGlobalEcdhShare && hasGlobalDleqProof) {
|
||||
ECKey globalEcdhShare = silentPaymentsEcdhShares.get(scanKey);
|
||||
SilentPaymentsDLEQProof globalProof = silentPaymentsDLEQProofs.get(scanKey);
|
||||
if(!globalProof.verify(summedPublicKey, scanKey, globalEcdhShare)) {
|
||||
throw new PSBTProofException("Global DLEQ proof verification failed for scan key: " + scanKey);
|
||||
}
|
||||
|
||||
try {
|
||||
SilentPaymentUtils.validateOutputAddresses(silentPayments, globalEcdhShare, summedPublicKey, outpoints);
|
||||
} catch(InvalidSilentPaymentException e) {
|
||||
throw new PSBTProofException(e);
|
||||
}
|
||||
} else if(hasGlobalEcdhShare || hasGlobalDleqProof) {
|
||||
throw new PSBTProofException("Global ECDH share and DLEQ proof must both be present for scan key: " + scanKey);
|
||||
} else {
|
||||
List<ECKey> inputEcdhShares = new ArrayList<>();
|
||||
for(PSBTInput input : getPsbtInputs()) {
|
||||
ECKey inputPublicKey = inputPublicKeys.entrySet().stream()
|
||||
.filter(entry -> entry.getKey().getIndex() == input.getIndex()).map(Map.Entry::getValue).findFirst().orElse(null);
|
||||
if(inputPublicKey != null) {
|
||||
boolean hasInputEcdhShare = input.getSilentPaymentsEcdhShares().containsKey(scanKey);
|
||||
boolean hasInputDleqProof = input.getSilentPaymentsDLEQProofs().containsKey(scanKey);
|
||||
if(!hasInputEcdhShare || !hasInputDleqProof) {
|
||||
throw new PSBTProofException("Eligible input at index " + input.getIndex() + " must provide PSBT_IN_SP_ECDH_SHARE and PSBT_IN_SP_DLEQ");
|
||||
}
|
||||
|
||||
ECKey inputEcdhShare = input.getSilentPaymentsEcdhShares().get(scanKey);
|
||||
SilentPaymentsDLEQProof inputProof = input.getSilentPaymentsDLEQProofs().get(scanKey);
|
||||
if(!inputProof.verify(inputPublicKey, scanKey, inputEcdhShare)) {
|
||||
throw new PSBTProofException("Input DLEQ proof verification for input at index " + input.getIndex() + " failed for scan key: " + scanKey);
|
||||
}
|
||||
inputEcdhShares.add(inputEcdhShare);
|
||||
}
|
||||
}
|
||||
|
||||
ECKey summedEcdhShare = SilentPaymentUtils.getSummedPublicKey(inputEcdhShares);
|
||||
if(summedEcdhShare == null) {
|
||||
throw new PSBTProofException("No ECDH shares found for scan key: " + scanKey);
|
||||
}
|
||||
|
||||
try {
|
||||
SilentPaymentUtils.validateOutputAddresses(silentPayments, summedEcdhShare, summedPublicKey, outpoints);
|
||||
} catch(InvalidSilentPaymentException e) {
|
||||
throw new PSBTProofException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasSignatures() {
|
||||
for(PSBTInput psbtInput : getPsbtInputs()) {
|
||||
if(!psbtInput.getPartialSignatures().isEmpty() || psbtInput.getTapKeyPathSignature() != null || psbtInput.getFinalScriptSig() != null || psbtInput.getFinalScriptWitness() != null) {
|
||||
@ -594,6 +744,12 @@ public class PSBT {
|
||||
if(modifiable != null) {
|
||||
entries.add(populateEntry(PSBT_GLOBAL_TX_MODIFIABLE, null, new byte[] { modifiable }));
|
||||
}
|
||||
for(Map.Entry<ECKey, ECKey> entry : silentPaymentsEcdhShares.entrySet()) {
|
||||
entries.add(populateEntry(PSBT_GLOBAL_SP_ECDH_SHARE, entry.getKey().getPubKey(), entry.getValue().getPubKey()));
|
||||
}
|
||||
for(Map.Entry<ECKey, SilentPaymentsDLEQProof> entry : silentPaymentsDLEQProofs.entrySet()) {
|
||||
entries.add(populateEntry(PSBT_GLOBAL_SP_DLEQ, entry.getKey().getPubKey(), entry.getValue().getBytes()));
|
||||
}
|
||||
}
|
||||
|
||||
if(version != null) {
|
||||
@ -681,6 +837,8 @@ public class PSBT {
|
||||
}
|
||||
|
||||
extendedPublicKeys.putAll(psbt.extendedPublicKeys);
|
||||
silentPaymentsEcdhShares.putAll(psbt.silentPaymentsEcdhShares);
|
||||
silentPaymentsDLEQProofs.putAll(psbt.silentPaymentsDLEQProofs);
|
||||
globalProprietary.putAll(psbt.globalProprietary);
|
||||
|
||||
for(int i = 0; i < getPsbtInputs().size(); i++) {
|
||||
@ -724,6 +882,17 @@ public class PSBT {
|
||||
}
|
||||
}
|
||||
|
||||
for(int i = 0; i < finalTransaction.getOutputs().size(); i++) {
|
||||
TransactionOutput txOutput = finalTransaction.getOutputs().get(i);
|
||||
PSBTOutput psbtOutput = getPsbtOutputs().get(i);
|
||||
if(psbtOutput.getSilentPaymentAddress() != null) {
|
||||
if(psbtOutput.script() == null || !ScriptType.P2TR.isScriptType(psbtOutput.script())) {
|
||||
throw new IllegalStateException("Silent payment output at index " + i + " must provide a valid P2TR PSBT_OUT_SCRIPT");
|
||||
}
|
||||
txOutput.setScriptBytes(psbtOutput.script().getProgram());
|
||||
}
|
||||
}
|
||||
|
||||
return finalTransaction;
|
||||
}
|
||||
|
||||
@ -803,7 +972,12 @@ public class PSBT {
|
||||
}
|
||||
}
|
||||
for(PSBTOutput psbtOutput : getPsbtOutputs()) {
|
||||
transaction.addOutput(psbtOutput.getAmount(), psbtOutput.getScript() == null ? new Script(new byte[0]) : psbtOutput.getScript());
|
||||
//For unsigned transactions set the output script to the serialized silent payments address if present as per BIP375
|
||||
if(psbtOutput.getSilentPaymentAddress() != null) {
|
||||
transaction.addOutput(psbtOutput.getAmount(), new Script(psbtOutput.getSilentPaymentAddress().serialize()));
|
||||
} else {
|
||||
transaction.addOutput(psbtOutput.getAmount(), psbtOutput.getScript() == null ? new Script(new byte[0]) : psbtOutput.getScript());
|
||||
}
|
||||
}
|
||||
return transaction;
|
||||
}
|
||||
@ -889,6 +1063,14 @@ public class PSBT {
|
||||
}
|
||||
}
|
||||
|
||||
public Map<ECKey, ECKey> getSilentPaymentsEcdhShares() {
|
||||
return silentPaymentsEcdhShares;
|
||||
}
|
||||
|
||||
public Map<ECKey, SilentPaymentsDLEQProof> getSilentPaymentsDLEQProofs() {
|
||||
return silentPaymentsDLEQProofs;
|
||||
}
|
||||
|
||||
public Map<String, String> getGlobalProprietary() {
|
||||
return globalProprietary;
|
||||
}
|
||||
@ -902,7 +1084,7 @@ public class PSBT {
|
||||
}
|
||||
|
||||
public String toBase64String(boolean includeXpubs) {
|
||||
return Base64.toBase64String(serialize(includeXpubs, true));
|
||||
return Base64.getEncoder().encodeToString(serialize(includeXpubs, true));
|
||||
}
|
||||
|
||||
public void convertVersion(int version) {
|
||||
@ -1028,7 +1210,7 @@ public class PSBT {
|
||||
if(Utils.isHex(s) && s.startsWith(PSBT_MAGIC_HEX)) {
|
||||
return true;
|
||||
} else {
|
||||
return Utils.isBase64(s) && Utils.bytesToHex(Base64.decode(s)).startsWith(PSBT_MAGIC_HEX);
|
||||
return Utils.isBase64(s) && Utils.bytesToHex(Base64.getDecoder().decode(s)).startsWith(PSBT_MAGIC_HEX);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
@ -1047,7 +1229,7 @@ public class PSBT {
|
||||
}
|
||||
|
||||
if (Utils.isBase64(strPSBT) && !Utils.isHex(strPSBT)) {
|
||||
strPSBT = Utils.bytesToHex(Base64.decode(strPSBT));
|
||||
strPSBT = Utils.bytesToHex(Base64.getDecoder().decode(strPSBT));
|
||||
}
|
||||
|
||||
byte[] psbtBytes = Utils.hexToBytes(strPSBT);
|
||||
|
||||
@ -10,10 +10,7 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
public class PSBTEntry {
|
||||
private final byte[] key;
|
||||
@ -80,6 +77,9 @@ public class PSBTEntry {
|
||||
}
|
||||
|
||||
public static KeyDerivation parseKeyDerivation(byte[] data) throws PSBTParseException {
|
||||
if(data.length == 0) {
|
||||
return new KeyDerivation(KeyDerivation.DEFAULT_WATCH_ONLY_FINGERPRINT, Collections.emptyList());
|
||||
}
|
||||
if(data.length < 4) {
|
||||
throw new PSBTParseException("Invalid master fingerprint specified: not enough bytes");
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentsDLEQProof;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -35,10 +36,12 @@ public class PSBTInput {
|
||||
public static final byte PSBT_IN_SEQUENCE = 0x10;
|
||||
public static final byte PSBT_IN_REQUIRED_TIME_LOCKTIME = 0x11;
|
||||
public static final byte PSBT_IN_REQUIRED_HEIGHT_LOCKTIME = 0x12;
|
||||
public static final byte PSBT_IN_PROPRIETARY = (byte)0xfc;
|
||||
public static final byte PSBT_IN_TAP_KEY_SIG = 0x13;
|
||||
public static final byte PSBT_IN_TAP_BIP32_DERIVATION = 0x16;
|
||||
public static final byte PSBT_IN_TAP_INTERNAL_KEY = 0x17;
|
||||
public static final byte PSBT_IN_SP_ECDH_SHARE = 0x1d;
|
||||
public static final byte PSBT_IN_SP_DLEQ = 0x1e;
|
||||
public static final byte PSBT_IN_PROPRIETARY = (byte)0xfc;
|
||||
|
||||
private final PSBT psbt;
|
||||
private Transaction nonWitnessUtxo;
|
||||
@ -60,12 +63,14 @@ public class PSBTInput {
|
||||
private Map<ECKey, Map<KeyDerivation, List<Sha256Hash>>> tapDerivedPublicKeys = new LinkedHashMap<>();
|
||||
private ECKey tapInternalKey;
|
||||
|
||||
//PSBTv2 fields
|
||||
//PSBTv2-only fields
|
||||
private Sha256Hash prevTxid;
|
||||
private Long prevIndex;
|
||||
private Long sequence;
|
||||
private Long requiredTimeLocktime;
|
||||
private Long requiredHeightLocktime;
|
||||
private final Map<ECKey, ECKey> silentPaymentsEcdhShares = new LinkedHashMap<>();
|
||||
private final Map<ECKey, SilentPaymentsDLEQProof> silentPaymentsDLEQProofs = new LinkedHashMap<>();
|
||||
|
||||
private int index;
|
||||
|
||||
@ -168,6 +173,9 @@ public class PSBTInput {
|
||||
break;
|
||||
case PSBT_IN_SIGHASH_TYPE:
|
||||
entry.checkOneByteKey();
|
||||
if(entry.getData().length != 4) {
|
||||
throw new PSBTParseException("PSBT input sighash type must be 4 bytes");
|
||||
}
|
||||
long sighashType = Utils.readUint32(entry.getData(), 0);
|
||||
SigHash sigHash = SigHash.fromByte((byte)sighashType);
|
||||
this.sigHash = sigHash;
|
||||
@ -284,16 +292,25 @@ public class PSBTInput {
|
||||
break;
|
||||
case PSBT_IN_OUTPUT_INDEX:
|
||||
entry.checkOneByteKey();
|
||||
if(entry.getData().length != 4) {
|
||||
throw new PSBTParseException("PSBT input output index must be 4 bytes");
|
||||
}
|
||||
this.prevIndex = Utils.readUint32(entry.getData(), 0);
|
||||
log.debug("Found input previous output index " + this.prevIndex);
|
||||
break;
|
||||
case PSBT_IN_SEQUENCE:
|
||||
entry.checkOneByteKey();
|
||||
if(entry.getData().length != 4) {
|
||||
throw new PSBTParseException("PSBT input sequence must be 4 bytes");
|
||||
}
|
||||
this.sequence = Utils.readUint32(entry.getData(), 0);
|
||||
log.debug("Found input sequence " + this.sequence);
|
||||
break;
|
||||
case PSBT_IN_REQUIRED_TIME_LOCKTIME:
|
||||
entry.checkOneByteKey();
|
||||
if(entry.getData().length != 4) {
|
||||
throw new PSBTParseException("PSBT input required time locktime must be 4 bytes");
|
||||
}
|
||||
long requiredTimeLocktime = Utils.readUint32(entry.getData(), 0);
|
||||
if(requiredTimeLocktime < 500000000) {
|
||||
throw new PSBTParseException("Required time locktime is less than 500000000");
|
||||
@ -303,6 +320,9 @@ public class PSBTInput {
|
||||
break;
|
||||
case PSBT_IN_REQUIRED_HEIGHT_LOCKTIME:
|
||||
entry.checkOneByteKey();
|
||||
if(entry.getData().length != 4) {
|
||||
throw new PSBTParseException("PSBT input required height locktime must be 4 bytes");
|
||||
}
|
||||
long requiredHeightLocktime = Utils.readUint32(entry.getData(), 0);
|
||||
if(requiredHeightLocktime >= 500000000) {
|
||||
throw new PSBTParseException("Required time locktime is greater than or equal to 500000000");
|
||||
@ -310,6 +330,26 @@ public class PSBTInput {
|
||||
this.requiredHeightLocktime = requiredHeightLocktime;
|
||||
log.debug("Found input required height locktime " + this.requiredHeightLocktime);
|
||||
break;
|
||||
case PSBT_IN_SP_ECDH_SHARE:
|
||||
entry.checkOneBytePlusPubKey();
|
||||
if(entry.getData().length != 33) {
|
||||
throw new PSBTParseException("PSBT input silent payments ECDH share data must be 33 bytes");
|
||||
}
|
||||
ECKey inputScanKey = ECKey.fromPublicOnly(entry.getKeyData());
|
||||
ECKey inputEcdhShare = ECKey.fromPublicOnly(entry.getData());
|
||||
this.silentPaymentsEcdhShares.put(inputScanKey, inputEcdhShare);
|
||||
log.debug("Found input silent payments ECDH share for scan key: " + Utils.bytesToHex(entry.getKeyData()));
|
||||
break;
|
||||
case PSBT_IN_SP_DLEQ:
|
||||
entry.checkOneBytePlusPubKey();
|
||||
if(entry.getData().length != 64) {
|
||||
throw new PSBTParseException("PSBT input silent payments DLEQ proof data must be 64 bytes");
|
||||
}
|
||||
ECKey inputProofScanKey = ECKey.fromPublicOnly(entry.getKeyData());
|
||||
SilentPaymentsDLEQProof inputDleqProof = SilentPaymentsDLEQProof.fromBytes(entry.getData());
|
||||
this.silentPaymentsDLEQProofs.put(inputProofScanKey, inputDleqProof);
|
||||
log.debug("Found input silent payments DLEQ proof for scan key: " + Utils.bytesToHex(entry.getKeyData()));
|
||||
break;
|
||||
case PSBT_IN_PROPRIETARY:
|
||||
this.proprietary.put(Utils.bytesToHex(entry.getKeyData()), Utils.bytesToHex(entry.getData()));
|
||||
log.debug("Found proprietary input " + Utils.bytesToHex(entry.getKeyData()) + ": " + Utils.bytesToHex(entry.getData()));
|
||||
@ -413,6 +453,12 @@ public class PSBTInput {
|
||||
Utils.uint32ToByteArrayLE(requiredHeightLocktime, requiredHeightLocktimeBytes, 0);
|
||||
entries.add(populateEntry(PSBT_IN_REQUIRED_HEIGHT_LOCKTIME, null, requiredHeightLocktimeBytes));
|
||||
}
|
||||
for(Map.Entry<ECKey, ECKey> entry : silentPaymentsEcdhShares.entrySet()) {
|
||||
entries.add(populateEntry(PSBT_IN_SP_ECDH_SHARE, entry.getKey().getPubKey(), entry.getValue().getPubKey()));
|
||||
}
|
||||
for(Map.Entry<ECKey, SilentPaymentsDLEQProof> entry : silentPaymentsDLEQProofs.entrySet()) {
|
||||
entries.add(populateEntry(PSBT_IN_SP_DLEQ, entry.getKey().getPubKey(), entry.getValue().getBytes()));
|
||||
}
|
||||
}
|
||||
|
||||
for(Map.Entry<String, String> entry : proprietary.entrySet()) {
|
||||
@ -501,6 +547,9 @@ public class PSBTInput {
|
||||
requiredHeightLocktime = psbtInput.requiredHeightLocktime;
|
||||
}
|
||||
|
||||
silentPaymentsEcdhShares.putAll(psbtInput.silentPaymentsEcdhShares);
|
||||
silentPaymentsDLEQProofs.putAll(psbtInput.silentPaymentsDLEQProofs);
|
||||
|
||||
proprietary.putAll(psbtInput.proprietary);
|
||||
|
||||
if(psbtInput.tapKeyPathSignature != null) {
|
||||
@ -732,6 +781,14 @@ public class PSBTInput {
|
||||
this.requiredHeightLocktime = requiredHeightLocktime;
|
||||
}
|
||||
|
||||
public Map<ECKey, ECKey> getSilentPaymentsEcdhShares() {
|
||||
return silentPaymentsEcdhShares;
|
||||
}
|
||||
|
||||
public Map<ECKey, SilentPaymentsDLEQProof> getSilentPaymentsDLEQProofs() {
|
||||
return silentPaymentsDLEQProofs;
|
||||
}
|
||||
|
||||
public boolean isSigned() {
|
||||
if(getTapKeyPathSignature() != null) {
|
||||
return true;
|
||||
@ -937,6 +994,10 @@ public class PSBTInput {
|
||||
return getWitnessUtxo() != null ? getWitnessUtxo() : (getNonWitnessUtxo() != null ? getNonWitnessUtxo().getOutputs().get(vout) : null);
|
||||
}
|
||||
|
||||
int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
void setIndex(int index) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ public class PSBTOutput {
|
||||
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_SP_V0_LABEL = 0x0a;
|
||||
public static final byte PSBT_OUT_DNSSEC_PROOF = 0x35;
|
||||
public static final byte PSBT_OUT_PROPRIETARY = (byte)0xfc;
|
||||
|
||||
@ -39,10 +40,11 @@ public class PSBTOutput {
|
||||
private ECKey tapInternalKey;
|
||||
private Map<String, byte[]> dnssecProof;
|
||||
|
||||
//PSBTv2 fields
|
||||
//PSBTv2-only fields
|
||||
private Long amount;
|
||||
private Script script;
|
||||
private SilentPaymentAddress silentPaymentAddress;
|
||||
private Long silentPaymentLabel;
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PSBTOutput.class);
|
||||
|
||||
@ -103,6 +105,9 @@ public class PSBTOutput {
|
||||
break;
|
||||
case PSBT_OUT_AMOUNT:
|
||||
entry.checkOneByteKey();
|
||||
if(entry.getData().length != 8) {
|
||||
throw new PSBTParseException("PSBT output amount must be 8 bytes");
|
||||
}
|
||||
this.amount = Utils.readInt64(entry.getData(), 0);
|
||||
log.debug("Found output amount " + this.amount);
|
||||
break;
|
||||
@ -144,6 +149,16 @@ public class PSBTOutput {
|
||||
byte[] spendKey = new byte[33];
|
||||
System.arraycopy(entry.getData(), 33, spendKey, 0, 33);
|
||||
this.silentPaymentAddress = new SilentPaymentAddress(ECKey.fromPublicOnly(scanKey), ECKey.fromPublicOnly(spendKey));
|
||||
log.debug("Found output silent payment address " + this.silentPaymentAddress);
|
||||
break;
|
||||
case PSBT_OUT_SP_V0_LABEL:
|
||||
entry.checkOneByteKey();
|
||||
if(entry.getData().length != 4) {
|
||||
throw new PSBTParseException("PSBT output silent payment label must be 4 bytes");
|
||||
}
|
||||
this.silentPaymentLabel = Utils.readUint32(entry.getData(), 0);
|
||||
log.debug("Found output silent payment label " + this.silentPaymentLabel);
|
||||
break;
|
||||
case PSBT_OUT_DNSSEC_PROOF:
|
||||
entry.checkOneByteKey();
|
||||
this.dnssecProof = parseDnssecProof(entry.getData());
|
||||
@ -181,6 +196,11 @@ public class PSBTOutput {
|
||||
if(silentPaymentAddress != null) {
|
||||
entries.add(populateEntry(PSBT_OUT_SP_V0_INFO, null, Utils.concat(silentPaymentAddress.getScanKey().getPubKey(), silentPaymentAddress.getSpendKey().getPubKey())));
|
||||
}
|
||||
if(silentPaymentLabel != null) {
|
||||
byte[] labelBytes = new byte[4];
|
||||
Utils.uint32ToByteArrayLE(silentPaymentLabel, labelBytes, 0);
|
||||
entries.add(populateEntry(PSBT_OUT_SP_V0_LABEL, null, labelBytes));
|
||||
}
|
||||
}
|
||||
|
||||
for(Map.Entry<String, String> entry : proprietary.entrySet()) {
|
||||
@ -223,13 +243,21 @@ public class PSBTOutput {
|
||||
script = psbtOutput.script;
|
||||
}
|
||||
|
||||
proprietary.putAll(psbtOutput.proprietary);
|
||||
|
||||
tapDerivedPublicKeys.putAll(psbtOutput.tapDerivedPublicKeys);
|
||||
|
||||
if(psbtOutput.tapInternalKey != null) {
|
||||
tapInternalKey = psbtOutput.tapInternalKey;
|
||||
}
|
||||
|
||||
if(psbtOutput.silentPaymentAddress != null) {
|
||||
silentPaymentAddress = psbtOutput.silentPaymentAddress;
|
||||
}
|
||||
|
||||
if(psbtOutput.silentPaymentLabel != null) {
|
||||
silentPaymentLabel = psbtOutput.silentPaymentLabel;
|
||||
}
|
||||
|
||||
proprietary.putAll(psbtOutput.proprietary);
|
||||
}
|
||||
|
||||
public Script getRedeemScript() {
|
||||
@ -316,6 +344,14 @@ public class PSBTOutput {
|
||||
this.silentPaymentAddress = silentPaymentAddress;
|
||||
}
|
||||
|
||||
public Long getSilentPaymentLabel() {
|
||||
return silentPaymentLabel;
|
||||
}
|
||||
|
||||
public void setSilentPaymentLabel(Long silentPaymentLabel) {
|
||||
this.silentPaymentLabel = silentPaymentLabel;
|
||||
}
|
||||
|
||||
public Map<String, byte[]> getDnssecProof() {
|
||||
return dnssecProof;
|
||||
}
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
package com.sparrowwallet.drongo.psbt;
|
||||
|
||||
public class PSBTProofException extends PSBTParseException {
|
||||
public PSBTProofException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public PSBTProofException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public PSBTProofException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public PSBTProofException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@ -5,9 +5,12 @@ import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.Bech32;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class SilentPaymentAddress {
|
||||
public static final int VERSION = 0;
|
||||
|
||||
private final ECKey scanAddress;
|
||||
private final ECKey spendAddress;
|
||||
|
||||
@ -26,7 +29,7 @@ public class SilentPaymentAddress {
|
||||
|
||||
public String getAddress() {
|
||||
byte[] keys = Utils.concat(scanAddress.getPubKey(), spendAddress.getPubKey());
|
||||
return Bech32.encode(Network.get().getSilentPaymentsAddressHrp(), 0, Bech32.Encoding.BECH32M, keys);
|
||||
return Bech32.encode(Network.get().getSilentPaymentsAddressHrp(), VERSION, Bech32.Encoding.BECH32M, keys);
|
||||
}
|
||||
|
||||
public static SilentPaymentAddress from(String address) {
|
||||
@ -40,7 +43,7 @@ public class SilentPaymentAddress {
|
||||
}
|
||||
|
||||
int witnessVersion = data.data[0];
|
||||
if(witnessVersion != 0) {
|
||||
if(witnessVersion != VERSION) {
|
||||
throw new UnsupportedOperationException("Unsupported silent payments address witness version");
|
||||
}
|
||||
|
||||
@ -83,4 +86,12 @@ public class SilentPaymentAddress {
|
||||
public int hashCode() {
|
||||
return getAddress().hashCode();
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(67);
|
||||
buffer.put((byte)VERSION);
|
||||
buffer.put(scanAddress.getPubKey());
|
||||
buffer.put(spendAddress.getPubKey());
|
||||
return buffer.array();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.drongo.silentpayments;
|
||||
|
||||
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.MnemonicException;
|
||||
@ -50,8 +51,8 @@ public class SilentPaymentUtils {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static List<ECKey> getInputPubKeys(Transaction tx, Map<HashIndex, Script> spentScriptPubKeys) {
|
||||
List<ECKey> keys = new ArrayList<>();
|
||||
public static Map<TransactionInput, ECKey> getInputPubKeys(Transaction tx, Map<HashIndex, Script> spentScriptPubKeys) {
|
||||
Map<TransactionInput, ECKey> inputKeys = new LinkedHashMap<>();
|
||||
for(TransactionInput input : tx.getInputs()) {
|
||||
HashIndex hashIndex = new HashIndex(input.getOutpoint().getHash(), input.getOutpoint().getIndex());
|
||||
Script scriptPubKey = spentScriptPubKeys.get(hashIndex);
|
||||
@ -82,7 +83,7 @@ public class SilentPaymentUtils {
|
||||
|
||||
ECKey pubKey = ScriptType.P2TR.getPublicKeyFromScript(scriptPubKey);
|
||||
if(pubKey.isCompressed()) {
|
||||
keys.add(pubKey);
|
||||
inputKeys.put(input, pubKey);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@ -92,7 +93,7 @@ public class SilentPaymentUtils {
|
||||
if(input.getWitness() != null && input.getWitness().getPushCount() == 2) {
|
||||
byte[] pubKey = input.getWitness().getPushes().getLast();
|
||||
if(pubKey != null && pubKey.length == 33) {
|
||||
keys.add(ECKey.fromPublicOnly(pubKey));
|
||||
inputKeys.put(input, ECKey.fromPublicOnly(pubKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -101,7 +102,7 @@ public class SilentPaymentUtils {
|
||||
if(input.getWitness() != null && input.getWitness().getPushCount() == 2) {
|
||||
byte[] pubKey = input.getWitness().getPushes().getLast();
|
||||
if(pubKey != null && pubKey.length == 33) {
|
||||
keys.add(ECKey.fromPublicOnly(pubKey));
|
||||
inputKeys.put(input, ECKey.fromPublicOnly(pubKey));
|
||||
}
|
||||
}
|
||||
break;
|
||||
@ -109,7 +110,7 @@ public class SilentPaymentUtils {
|
||||
byte[] spkHash = ScriptType.P2PKH.getHashFromScript(scriptPubKey);
|
||||
for(ScriptChunk scriptChunk : input.getScriptSig().getChunks()) {
|
||||
if(scriptChunk.isPubKey() && scriptChunk.getData().length == 33 && Arrays.equals(Utils.sha256hash160(scriptChunk.getData()), spkHash)) {
|
||||
keys.add(scriptChunk.getPubKey());
|
||||
inputKeys.put(input, scriptChunk.getPubKey());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -121,7 +122,7 @@ public class SilentPaymentUtils {
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
return inputKeys;
|
||||
}
|
||||
|
||||
public static boolean containsTaprootOutput(Transaction tx) {
|
||||
@ -164,7 +165,7 @@ public class SilentPaymentUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<ECKey> inputKeys = getInputPubKeys(tx, spentScriptPubKeys);
|
||||
Map<TransactionInput, ECKey> inputKeys = getInputPubKeys(tx, spentScriptPubKeys);
|
||||
if(inputKeys.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
@ -175,8 +176,9 @@ public class SilentPaymentUtils {
|
||||
|
||||
try {
|
||||
byte[][] inputPubKeys = new byte[inputKeys.size()][];
|
||||
for(int i = 0; i < inputPubKeys.length; i++) {
|
||||
inputPubKeys[i] = inputKeys.get(i).getPubKey(true);
|
||||
int index = 0;
|
||||
for (ECKey key : inputKeys.values()) {
|
||||
inputPubKeys[index++] = key.getPubKey(true);
|
||||
}
|
||||
byte[] combinedPubKey = NativeSecp256k1.pubKeyCombine(inputPubKeys, true);
|
||||
byte[] smallestOutpoint = tx.getInputs().stream().map(input -> input.getOutpoint().bitcoinSerialize()).min(new Utils.LexicographicByteArrayComparator()).orElseThrow();
|
||||
@ -190,6 +192,16 @@ public class SilentPaymentUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the output addresses for a list of silent payments by calculating the shared secret
|
||||
* between scan keys, spend keys, and the summed private key derived from the provided UTXOs.
|
||||
* Updates each silent payment instance with the corresponding address.
|
||||
*
|
||||
* @param silentPayments the list of silent payments containing silent payment addresses and metadata
|
||||
* @param utxos a map of UTXOs (unspent transaction outputs) to wallet nodes, containing information
|
||||
* about inputs used to derive the summed private key
|
||||
* @throws InvalidSilentPaymentException if the computed shared secrets or addresses are invalid
|
||||
*/
|
||||
public static void computeOutputAddresses(List<SilentPayment> silentPayments, Map<HashIndex, WalletNode> utxos) throws InvalidSilentPaymentException {
|
||||
ECKey summedPrivateKey = getSummedPrivateKey(utxos.values());
|
||||
BigInteger inputHash = getInputHash(utxos.keySet(), summedPrivateKey);
|
||||
@ -212,6 +224,44 @@ public class SilentPaymentUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the output scripts for silent payment outputs match the expected scripts
|
||||
* computed from the ECDH shares. This implements BIP-375 output script verification.
|
||||
*
|
||||
* @param silentPayments List of silent payments sending to a common scan key
|
||||
* @param ecdhShare The ECDH share (a * B_scan), either global or summed from per-input
|
||||
* @param summedPublicKey The sum of all eligible input public keys
|
||||
* @param outpoints Set of outpoints for eligible inputs
|
||||
* @throws InvalidSilentPaymentException if validation fails or scripts don't match
|
||||
*/
|
||||
public static void validateOutputAddresses(List<SilentPayment> silentPayments, ECKey ecdhShare, ECKey summedPublicKey, Set<HashIndex> outpoints) throws InvalidSilentPaymentException {
|
||||
BigInteger inputHash = SilentPaymentUtils.getInputHash(outpoints, summedPublicKey);
|
||||
Map<ECKey, List<SilentPayment>> scanKeyGroups = SilentPaymentUtils.getScanKeyGroups(silentPayments);
|
||||
for(Map.Entry<ECKey, List<SilentPayment>> scanKeyGroup : scanKeyGroups.entrySet()) {
|
||||
// Compute shared secret from ECDH share and input hash
|
||||
// Instead of: sharedSecret = scanKey.multiply(inputHash).multiply(summedPrivateKey.getPrivKey())
|
||||
// We use: sharedSecret = ecdhShare.multiply(inputHash)
|
||||
// Because ecdhShare is already (a * B_scan)
|
||||
ECKey sharedSecret = ecdhShare.multiply(inputHash);
|
||||
|
||||
int k = 0;
|
||||
for(SilentPayment silentPayment : scanKeyGroup.getValue()) {
|
||||
BigInteger tk = new BigInteger(1, Utils.taggedHash(SilentPaymentUtils.BIP_0352_SHARED_SECRET_TAG,
|
||||
Utils.concat(sharedSecret.getPubKey(true), ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(k).array())));
|
||||
if(tk.equals(BigInteger.ZERO) || tk.compareTo(ECKey.CURVE.getCurve().getOrder()) >= 0) {
|
||||
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);
|
||||
Address expectedAddress = ScriptType.P2TR.getAddress(pkm.getPubKeyXCoord());
|
||||
if(!silentPayment.getAddress().equals(expectedAddress)) {
|
||||
throw new InvalidSilentPaymentException("Silent payment output address mismatch: expected " + expectedAddress + " but got " + silentPayment.getAddress());
|
||||
}
|
||||
k++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<ECKey, List<SilentPayment>> getScanKeyGroups(Collection<SilentPayment> silentPayments) {
|
||||
Map<ECKey, List<SilentPayment>> scanKeyGroups = new LinkedHashMap<>();
|
||||
for(SilentPayment silentPayment : silentPayments) {
|
||||
@ -223,9 +273,9 @@ public class SilentPaymentUtils {
|
||||
return scanKeyGroups;
|
||||
}
|
||||
|
||||
public static BigInteger getInputHash(Set<HashIndex> outpoints, ECKey summedPrivateKey) throws InvalidSilentPaymentException {
|
||||
public static BigInteger getInputHash(Set<HashIndex> outpoints, ECKey summedInputKey) throws InvalidSilentPaymentException {
|
||||
byte[] smallestOutpoint = getSmallestOutpoint(outpoints);
|
||||
byte[] concat = Utils.concat(smallestOutpoint, summedPrivateKey.getPubKey());
|
||||
byte[] concat = Utils.concat(smallestOutpoint, summedInputKey.getPubKey(true));
|
||||
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 InvalidSilentPaymentException("The input hash is invalid for the eligible silent payments inputs");
|
||||
@ -267,6 +317,22 @@ public class SilentPaymentUtils {
|
||||
return summedPrivateKey;
|
||||
}
|
||||
|
||||
public static ECKey getSummedPublicKey(Collection<ECKey> publicKeys) {
|
||||
ECKey summedKey = null;
|
||||
|
||||
for(ECKey publicKey : publicKeys) {
|
||||
if(publicKey != null) {
|
||||
if(summedKey == null) {
|
||||
summedKey = publicKey;
|
||||
} else {
|
||||
summedKey = summedKey.add(publicKey, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return summedKey;
|
||||
}
|
||||
|
||||
public static byte[] getSmallestOutpoint(Set<HashIndex> outpoints) {
|
||||
return outpoints.stream().map(outpoint -> new TransactionOutPoint(outpoint.getHash(), outpoint.getIndex())).map(TransactionOutPoint::bitcoinSerialize)
|
||||
.min(new Utils.LexicographicByteArrayComparator()).orElseThrow(() -> new IllegalArgumentException("No inputs provided to calculate silent payments input hash"));
|
||||
|
||||
@ -1677,7 +1677,6 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||
|
||||
Script outputScript = silentPayment.getAddress().getOutputScript();
|
||||
silentOutput.setScript(outputScript);
|
||||
silentOutput.getOutput().setScriptBytes(outputScript.getProgram());
|
||||
addSilentPaymentAddress(silentPayment.getAddress(), silentPayment.getSilentPaymentAddress());
|
||||
}
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@ -718,8 +719,194 @@ public class PSBTTest {
|
||||
Assertions.assertEquals(origPsbtv2.getTransaction().getTxId(), psbtv0.getTransaction().getTxId());
|
||||
}
|
||||
|
||||
// BIP-375 Silent Payments Tests
|
||||
|
||||
@Test
|
||||
public void invalidMissingDleqProof() throws PSBTParseException {
|
||||
String psbt = "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAwABDiCrtYht20vGCALx8ZiisSkDZZzJ7nPgIx1FVehBiNyWQAEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NACIdAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4IQJVFk55JtUNUqCf+ZBkel6Vwdsb/GimFvvC2iE5J/mL/wEDBAEAAAAAAQMIGHMBAAAAAAABBCJRIBe7uqUS4DD7yj7lFPeyOAfhEYMaR0VEcJE0yfm/RrbEAQlCAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4Ak1Rg1P0vRjXac9o/2LvEGabcIYkawpkA/5XveSSEUSLAA==";
|
||||
PSBT parsed = PSBT.fromString(psbt);
|
||||
|
||||
Map<TransactionInput, ECKey> inputPublicKeys = new LinkedHashMap<>();
|
||||
TransactionInput input0 = parsed.getTransaction().getInputs().get(0);
|
||||
ECKey pubKey0 = ECKey.fromPublicOnly(Hex.decode("03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d"));
|
||||
inputPublicKeys.put(input0, pubKey0);
|
||||
|
||||
Assertions.assertThrows(PSBTProofException.class, () -> parsed.validateSilentPayments(inputPublicKeys));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidDleqProof() throws PSBTParseException {
|
||||
String psbt = "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAwABDiBJpgB4JAmRsNt6srOq9JqFCEYGmw7mPo8/4lJ2+/nPoAEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NACIdAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4IQJVFk55JtUNUqCf+ZBkel6Vwdsb/GimFvvC2iE5J/mL/yIeAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4QIHV1pvvebv/E/qjiAN4whI/xJQnstvRxMFrJvVYN5hmLW7l6QbgreDug7YKivXVjkDvnwa9fLacXS3HUTu7/mEBAwQBAAAAAAEDCBhzAQAAAAAAAQQiUSCy5HLRM5RYz18oPOTQydTDNj9R1Kn1KSQfhJ3O1Ft10AEJQgLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+AJNUYNT9L0Y12nPaP9i7xBmm3CGJGsKZAP+V73kkhFEiwA=";
|
||||
PSBT parsed = PSBT.fromString(psbt);
|
||||
|
||||
Map<TransactionInput, ECKey> inputPublicKeys = new LinkedHashMap<>();
|
||||
TransactionInput input0 = parsed.getTransaction().getInputs().get(0);
|
||||
ECKey pubKey0 = ECKey.fromPublicOnly(Hex.decode("03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d"));
|
||||
inputPublicKeys.put(input0, pubKey0);
|
||||
|
||||
Assertions.assertThrows(PSBTProofException.class, () -> parsed.validateSilentPayments(inputPublicKeys));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidNonSighashAll() throws PSBTParseException {
|
||||
String psbt = "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAwABDiCfF8iU72tyS4+7lJj0TTc8KFCARRz/QDHgOTLIuPm2twEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NAAEDBAIAAAAiHQLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+CECVRZOeSbVDVKgn/mQZHpelcHbG/xophb7wtohOSf5i/8iHgLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+EB82fZvBIlT42he3KPxdpkVJZi87KRvIU7SuM39pJ22oAePiUtm39mKZwbmhzrySfXfZOzJhETpE3KXfZb62CtDAAEDCBhzAQAAAAAAAQQiUSB0TgmVHxe1gkg8egwIwGJv6sNmaTyE+vOeA0thLejccQEJQgLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+AJNUYNT9L0Y12nPaP9i7xBmm3CGJGsKZAP+V73kkhFEiwA=";
|
||||
PSBT parsed = PSBT.fromString(psbt);
|
||||
|
||||
Map<TransactionInput, ECKey> inputPublicKeys = new LinkedHashMap<>();
|
||||
TransactionInput input0 = parsed.getTransaction().getInputs().get(0);
|
||||
ECKey pubKey0 = ECKey.fromPublicOnly(Hex.decode("03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d"));
|
||||
inputPublicKeys.put(input0, pubKey0);
|
||||
|
||||
Assertions.assertThrows(PSBTProofException.class, () -> parsed.validateSilentPayments(inputPublicKeys));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidMixedSegwitVersions() {
|
||||
String psbt = "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAwABDiCuv6p7LnQnsTi6F7UesBJ3TJOXBoHuPhb2Y3hjUklAJAEPBAAAAAABASughgEAAAAAACJSIIYiz2yIzTx5hSOFG+T1WYNgEYY+sDBYObtzXueh9KPpARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NAAEDBAEAAAAiHQLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+CECVRZOeSbVDVKgn/mQZHpelcHbG/xophb7wtohOSf5i/8iHgLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+EDB1n84eIK/gXkV6hWCHWTVs4NcH8BcVYzj2CSSoX2QjBBIfbub3cEIDIwtnBlxsmYIPGkFTiIZPyRBKKxmc35gAAEDCBhzAQAAAAAAAQQiUSAhnr95bXw7ml6Di0tr4pd8dOuNav5vT1iABrPWw2HrfAEJQgLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+AJNUYNT9L0Y12nPaP9i7xBmm3CGJGsKZAP+V73kkhFEiwA=";
|
||||
Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(psbt));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidNoEcdhShares() throws PSBTParseException {
|
||||
String psbt = "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAwABDiAkKwMCMdEgYFJ2eRmpLxfpzUNydadt47rD+2VnyyekwwEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NAAEDBAEAAAAAAQMIGHMBAAAAAAABBCJRIATj08VSBMBgg9/eWjaW0ZzOJ28erl5eCzDe6FqZrylWAQlCAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4Ak1Rg1P0vRjXac9o/2LvEGabcIYkawpkA/5XveSSEUSLAA==";
|
||||
PSBT parsed = PSBT.fromString(psbt);
|
||||
|
||||
Map<TransactionInput, ECKey> inputPublicKeys = new LinkedHashMap<>();
|
||||
TransactionInput input0 = parsed.getTransaction().getInputs().get(0);
|
||||
ECKey pubKey0 = ECKey.fromPublicOnly(Hex.decode("03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d"));
|
||||
inputPublicKeys.put(input0, pubKey0);
|
||||
|
||||
Assertions.assertThrows(PSBTProofException.class, () -> parsed.validateSilentPayments(inputPublicKeys));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidGlobalEcdhShareWithoutDleq() throws PSBTParseException {
|
||||
String psbt = "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAyIHAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4IQJVFk55JtUNUqCf+ZBkel6Vwdsb/GimFvvC2iE5J/mL/wABDiD2W3/BmfoPsrzNsfoGEte1D2K0+PqV1WXEoB9MWC6SpAEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NAAEDBAEAAAAAAQMIGHMBAAAAAAABBCJRIIEAAjnkcLtSN9n6vrAZ4nmPxFcueck5C10dXjYbt+AgAQlCAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4Ak1Rg1P0vRjXac9o/2LvEGabcIYkawpkA/5XveSSEUSLAA==";
|
||||
PSBT parsed = PSBT.fromString(psbt);
|
||||
|
||||
Map<TransactionInput, ECKey> inputPublicKeys = new LinkedHashMap<>();
|
||||
TransactionInput input0 = parsed.getTransaction().getInputs().get(0);
|
||||
ECKey pubKey0 = ECKey.fromPublicOnly(Hex.decode("03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d"));
|
||||
inputPublicKeys.put(input0, pubKey0);
|
||||
|
||||
Assertions.assertThrows(PSBTProofException.class, () -> parsed.validateSilentPayments(inputPublicKeys));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidWrongSpV0InfoSize() {
|
||||
String psbt = "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAwABDiBhb07a0EqylXgp4bK5tBHX2y2Sc2xQAy4NalHiyZy13gEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NAAEDBAEAAAAiHQLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+CECVRZOeSbVDVKgn/mQZHpelcHbG/xophb7wtohOSf5i/8iHgLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+EDB1n84eIK/gXkV6hWCHWTVs4NcH8BcVYzj2CSSoX2QjBBIfbub3cEIDIwtnBlxsmYIPGkFTiIZPyRBKKxmc35gAAEDCBhzAQAAAAAAAQQiUSCrH00eEXl4YXYOd4rbbMFm+Wq2HdGvh5nmUfQVMiHskwEJQQLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+AJNUYNT9L0Y12nPaP9i7xBmm3CGJGsKZAP+V73kkhFEAA==";
|
||||
Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(psbt));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidWrongEcdhShareSize() throws PSBTParseException {
|
||||
String psbt = "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAwABDiC1ovhHRrNOgzq0ftE7S5hwtBwhRvo0a3NKnEftiiROwwEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NAAEDBAEAAAAiHQLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+CACVRZOeSbVDVKgn/mQZHpelcHbG/xophb7wtohOSf5iwABAwgYcwEAAAAAAAEEIlEggDCSSC8mKdKdmFkQsILGjKIHwO1APY2HsBzp079x+AEBCUIC0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPgCTVGDU/S9GNdpz2j/Yu8QZptwhiRrCmQD/le95JIRRIsA";
|
||||
Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(psbt));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidWrongDleqSize() throws PSBTParseException {
|
||||
String psbt = "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAwABDiA78BpakVw0yvjzRht09tYBDcMoUSAE43p653aHic5dEwEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NAAEDBAEAAAAiHQLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+CECVRZOeSbVDVKgn/mQZHpelcHbG/xophb7wtohOSf5i/8iHgLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+D8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQMIGHMBAAAAAAABBCJRIO10/QbweN3s8W6gOiwTeFJjNgwtIQhX77ERqoAxaa37AQlCAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4Ak1Rg1P0vRjXac9o/2LvEGabcIYkawpkA/5XveSSEUSLAA==";
|
||||
Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(psbt));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidAddressMismatch() throws PSBTParseException {
|
||||
String psbt = "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAwABDiBumss8ldbK7k1KciUCaeuPPQVy7U+E9Lf8M6mavj46BQEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NAAEDBAEAAAAiHQLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+CECVRZOeSbVDVKgn/mQZHpelcHbG/xophb7wtohOSf5i/8iHgLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+EDB1n84eIK/gXkV6hWCHWTVs4NcH8BcVYzj2CSSoX2QjBBIfbub3cEIDIwtnBlxsmYIPGkFTiIZPyRBKKxmc35gAAEDCBhzAQAAAAAAAQQiUSDN452LBbSW+PGPILJ9CvmjIzMJ4EciGeCBP/O103h+KQEJQgLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+AJNUYNT9L0Y12nPaP9i7xBmm3CGJGsKZAP+V73kkhFEiwA=";
|
||||
PSBT parsed = PSBT.fromString(psbt);
|
||||
|
||||
Map<TransactionInput, ECKey> inputPublicKeys = new LinkedHashMap<>();
|
||||
TransactionInput input0 = parsed.getTransaction().getInputs().get(0);
|
||||
ECKey pubKey0 = ECKey.fromPublicOnly(Hex.decode("03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d"));
|
||||
inputPublicKeys.put(input0, pubKey0);
|
||||
|
||||
Assertions.assertThrows(PSBTProofException.class, () -> parsed.validateSilentPayments(inputPublicKeys));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validMixedInputTypes() throws PSBTParseException {
|
||||
String psbt = "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAIAAAABBQQBAAAAAQYBAwABDiApw6Peut1f6dS0XD295x0CWioK+UM7tO+6i0Pv5za0oQEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NACIdAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4IQJVFk55JtUNUqCf+ZBkel6Vwdsb/GimFvvC2iE5J/mL/yIeAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4QMHWfzh4gr+BeRXqFYIdZNWzg1wfwFxVjOPYJJKhfZCMEEh9u5vdwQgMjC2cGXGyZgg8aQVOIhk/JEEorGZzfmAAAQ4gtsz2qm3pd5sGLvsbwVMcZaXnsq6QVaZiqwVAbw30R9QBDwQAAAAAARAE/v///wEAUwIAAAAB5D9MfaDZ74MIM0F6Sy1pXgWgdanUwFY3tfzq3xlflbIAAAAAAP////8B8EkCAAAAAAAXqRRjd6+3VLmKdhIZzxZ2/JLCN4RQtocAAAAAAQRHUiECjx0ILWAB+kuomaQD2vmx2wG+klwiUzr2MO5kk7C+n3khAzWJfsWucE/lXY6l1Gewrr2zeZvhDDYEcdm3icgRr947Uq4AAQMIkF8BAAAAAAABBCJRIOwCLPQ2taebtxfBsueBADxLQswPLMCEEteHBizKo0eDAQlCAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4Ak1Rg1P0vRjXac9o/2LvEGabcIYkawpkA/5XveSSEUSLAA==";
|
||||
PSBT parsed = PSBT.fromString(psbt);
|
||||
|
||||
Map<TransactionInput, ECKey> inputPublicKeys = new LinkedHashMap<>();
|
||||
TransactionInput input0 = parsed.getTransaction().getInputs().get(0);
|
||||
ECKey pubKey0 = ECKey.fromPublicOnly(Hex.decode("03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d"));
|
||||
inputPublicKeys.put(input0, pubKey0);
|
||||
|
||||
parsed.validateSilentPayments(inputPublicKeys);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validBothGlobalAndInputEcdh() throws PSBTParseException {
|
||||
String psbt = "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAyIHAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4IQJVFk55JtUNUqCf+ZBkel6Vwdsb/GimFvvC2iE5J/mL/yIIAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4QMHWfzh4gr+BeRXqFYIdZNWzg1wfwFxVjOPYJJKhfZCMEEh9u5vdwQgMjC2cGXGyZgg8aQVOIhk/JEEorGZzfmAAAQ4gXPiMRKRM1SJjYb7RB1GUb5+HCVIpEeB3gV9ffweFr2kBDwQAAAAAAQEfoIYBAAAAAAAWABT42vdq2AOw76ldbP+K7giR068cjAEQBP7///8iBgPTV/fAcY8keOP9j4zMJynd2MDMrrHwKxgab0TUO5+NjQABAwQBAAAAIh0C0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPghAlUWTnkm1Q1SoJ/5kGR6XpXB2xv8aKYW+8LaITkn+Yv/Ih4C0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPhAwdZ/OHiCv4F5FeoVgh1k1bODXB/AXFWM49gkkqF9kIwQSH27m93BCAyMLZwZcbJmCDxpBU4iGT8kQSisZnN+YAABAwgYcwEAAAAAAAEEIlEgS6fi6UvLAo37uSgZaLV+3Cn2cFex/LIX3bRQwxpRxwwBCUIC0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPgCTVGDU/S9GNdpz2j/Yu8QZptwhiRrCmQD/le95JIRRIsA";
|
||||
PSBT parsed = PSBT.fromString(psbt);
|
||||
|
||||
Map<TransactionInput, ECKey> inputPublicKeys = new LinkedHashMap<>();
|
||||
TransactionInput input0 = parsed.getTransaction().getInputs().get(0);
|
||||
ECKey pubKey0 = ECKey.fromPublicOnly(Hex.decode("03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d"));
|
||||
inputPublicKeys.put(input0, pubKey0);
|
||||
|
||||
parsed.validateSilentPayments(inputPublicKeys);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validSingleSignerGlobalEcdh() throws PSBTParseException {
|
||||
String psbt = "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAyIHAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4IQJVFk55JtUNUqCf+ZBkel6Vwdsb/GimFvvC2iE5J/mL/yIIAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4QMHWfzh4gr+BeRXqFYIdZNWzg1wfwFxVjOPYJJKhfZCMEEh9u5vdwQgMjC2cGXGyZgg8aQVOIhk/JEEorGZzfmAAAQ4gbprLPJXWyu5NSnIlAmnrjz0Fcu1PhPS3/DOpmr4+OgUBDwQAAAAAAQEfoIYBAAAAAAAWABT42vdq2AOw76ldbP+K7giR068cjAEQBP7///8iBgPTV/fAcY8keOP9j4zMJynd2MDMrrHwKxgab0TUO5+NjQABAwQBAAAAAAEDCBhzAQAAAAAAAQQiUSCuGfvuJzChqVLX0lmMxwP93zuXKyUUix7Rp5roc51eBwEJQgLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+AJNUYNT9L0Y12nPaP9i7xBmm3CGJGsKZAP+V73kkhFEiwA=";
|
||||
PSBT parsed = PSBT.fromString(psbt);
|
||||
|
||||
Map<TransactionInput, ECKey> inputPublicKeys = new LinkedHashMap<>();
|
||||
TransactionInput input0 = parsed.getTransaction().getInputs().get(0);
|
||||
ECKey pubKey0 = ECKey.fromPublicOnly(Hex.decode("03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d"));
|
||||
inputPublicKeys.put(input0, pubKey0);
|
||||
|
||||
parsed.validateSilentPayments(inputPublicKeys);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validMultiPartyPerInputEcdh() throws PSBTParseException {
|
||||
String psbt = "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAIAAAABBQQBAAAAAQYBAwABDiBc+IxEpEzVImNhvtEHUZRvn4cJUikR4HeBX19/B4WvaQEPBAAAAAABAR9QwwAAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NAAEDBAEAAAAiHQLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+CECVRZOeSbVDVKgn/mQZHpelcHbG/xophb7wtohOSf5i/8iHgLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+EDB1n84eIK/gXkV6hWCHWTVs4NcH8BcVYzj2CSSoX2QjBBIfbub3cEIDIwtnBlxsmYIPGkFTiIZPyRBKKxmc35gAAEOIBPS7rrZoQmRR0EMhqb+CyDunbdZ2kHP1O6Xks1QEjcVAQ8EAAAAAAEBH1DDAAAAAAAAFgAUQhxxWu35g68OO2dv98SU0QVPzboBEAT+////IgYCjx0ILWAB+kuomaQD2vmx2wG+klwiUzr2MO5kk7C+n3kAAQMEAQAAACIdAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4IQNb6DQQKcckf6K5ONXUuFs6afvZMXutha9leZJx3LQoASIeAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4QMD+LMxS37uSW9P/ZUt6ltjsDgMaZ9BBtPmcsOgRSeKHqCvT1RvA/3M6zn0yq2PWEhhlRXeAj7LH+wbSvgb/OrYAAQMIGHMBAAAAAAABBCJRIAvcah2ruHUXZ4wrX33N/yl23F0RNcTdk/ZnI+vvuY4lAQlCAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4Ak1Rg1P0vRjXac9o/2LvEGabcIYkawpkA/5XveSSEUSLAA==";
|
||||
PSBT parsed = PSBT.fromString(psbt);
|
||||
|
||||
Map<TransactionInput, ECKey> inputPublicKeys = new LinkedHashMap<>();
|
||||
TransactionInput input0 = parsed.getTransaction().getInputs().get(0);
|
||||
ECKey pubKey0 = ECKey.fromPublicOnly(Hex.decode("03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d"));
|
||||
inputPublicKeys.put(input0, pubKey0);
|
||||
|
||||
TransactionInput input1 = parsed.getTransaction().getInputs().get(1);
|
||||
ECKey pubKey1 = ECKey.fromPublicOnly(Hex.decode("028f1d082d6001fa4ba899a403daf9b1db01be925c22533af630ee6493b0be9f79"));
|
||||
inputPublicKeys.put(input1, pubKey1);
|
||||
|
||||
parsed.validateSilentPayments(inputPublicKeys);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validSilentPaymentWithChange() throws PSBTParseException {
|
||||
String psbt = "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQCAAAAAQYBAwABDiAlbK6m2hWAb7hW7a50mI1EDHqxtcCGHsgR0ZCSdHudHAEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NAAEDBAEAAAAiHQLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+CECVRZOeSbVDVKgn/mQZHpelcHbG/xophb7wtohOSf5i/8iHgLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+EDB1n84eIK/gXkV6hWCHWTVs4NcH8BcVYzj2CSSoX2QjBBIfbub3cEIDIwtnBlxsmYIPGkFTiIZPyRBKKxmc35gAAEDCFDDAAAAAAAAAQQiUSBVuRZLw33Ib1uJNhaCqjCItbP6U9rbQ+lfG9ETm7HANQEJQgLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+AP1JENIUgFlZrxF0fpqXFoYYs3ZM/FchDtCcjgi9SUyNwEKBAEAAAAAAQMIyK8AAAAAAAABBBYAFOPDEMwq86xuYsrkvSPj7lK54clZIgID01f3wHGPJHjj/Y+MzCcp3djAzK6x8CsYGm9E1DufjY0MAAAAAAAAAAAAAAABAA==";
|
||||
PSBT parsed = PSBT.fromString(psbt);
|
||||
|
||||
Map<TransactionInput, ECKey> inputPublicKeys = new LinkedHashMap<>();
|
||||
TransactionInput input0 = parsed.getTransaction().getInputs().get(0);
|
||||
ECKey pubKey0 = ECKey.fromPublicOnly(Hex.decode("03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d"));
|
||||
inputPublicKeys.put(input0, pubKey0);
|
||||
|
||||
parsed.validateSilentPayments(inputPublicKeys);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validMultipleOutputsSameScanKey() throws PSBTParseException {
|
||||
String psbt = "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQCAAAAAQYBAyIHAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4IQJVFk55JtUNUqCf+ZBkel6Vwdsb/GimFvvC2iE5J/mL/yIIAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4QMHWfzh4gr+BeRXqFYIdZNWzg1wfwFxVjOPYJJKhfZCMEEh9u5vdwQgMjC2cGXGyZgg8aQVOIhk/JEEorGZzfmAAAQ4gLHvTL/FQccCuAyc4ZKFDbIpWITVp4RMtz46nPsjDMiIBDwQAAAAAAQEfoIYBAAAAAAAWABT42vdq2AOw76ldbP+K7giR068cjAEQBP7///8iBgPTV/fAcY8keOP9j4zMJynd2MDMrrHwKxgab0TUO5+NjQABAwQBAAAAAAEDCECcAAAAAAAAAQQiUSD7K3E6/VK6JHGBuZhBqk8siFW6332s4usuMFXaE+4rjAEJQgLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+AJNUYNT9L0Y12nPaP9i7xBmm3CGJGsKZAP+V73kkhFEiwABAwjY1gAAAAAAAAEEIlEgWaCp4b2YmHQgE1U4YO0LRLcm+8Ug6cVxvEZJXyR59hgBCUIC0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPgCTVGDU/S9GNdpz2j/Yu8QZptwhiRrCmQD/le95JIRRIsA";
|
||||
PSBT parsed = PSBT.fromString(psbt);
|
||||
|
||||
Map<TransactionInput, ECKey> inputPublicKeys = new LinkedHashMap<>();
|
||||
TransactionInput input0 = parsed.getTransaction().getInputs().get(0);
|
||||
ECKey pubKey0 = ECKey.fromPublicOnly(Hex.decode("03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d"));
|
||||
inputPublicKeys.put(input0, pubKey0);
|
||||
|
||||
parsed.validateSilentPayments(inputPublicKeys);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() throws Exception {
|
||||
public void tearDown() {
|
||||
Network.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user