use of com.sparrowwallet.drongo.psbt.PSBTOutput in project drongo by sparrowwallet.
the class PSBT method getPublicCopy.
public PSBT getPublicCopy() {
try {
PSBT publicCopy = new PSBT(serialize());
publicCopy.extendedPublicKeys.clear();
publicCopy.globalProprietary.clear();
for (PSBTInput psbtInput : publicCopy.getPsbtInputs()) {
psbtInput.getDerivedPublicKeys().clear();
psbtInput.getProprietary().clear();
}
for (PSBTOutput psbtOutput : publicCopy.getPsbtOutputs()) {
psbtOutput.getDerivedPublicKeys().clear();
psbtOutput.getProprietary().clear();
}
return publicCopy;
} catch (PSBTParseException e) {
throw new IllegalStateException("Could not parse PSBT", e);
}
}
use of com.sparrowwallet.drongo.psbt.PSBTOutput in project drongo by sparrowwallet.
the class PSBT method parseOutputEntries.
private void parseOutputEntries(List<List<PSBTEntry>> outputEntryLists) throws PSBTParseException {
for (List<PSBTEntry> outputEntries : outputEntryLists) {
PSBTEntry duplicate = findDuplicateKey(outputEntries);
if (duplicate != null) {
throw new PSBTParseException("Found duplicate key for PSBT output: " + Utils.bytesToHex(duplicate.getKey()));
}
PSBTOutput output = new PSBTOutput(outputEntries);
this.psbtOutputs.add(output);
}
}
use of com.sparrowwallet.drongo.psbt.PSBTOutput in project sparrow by sparrowwallet.
the class Payjoin method checkProposal.
private void checkProposal(PSBT original, PSBT proposal, int changeOutputIndex, long maxAdditionalFeeContribution, boolean allowOutputSubstitution) throws PayjoinReceiverException {
Queue<Map.Entry<TransactionInput, PSBTInput>> originalInputs = new ArrayDeque<>();
for (int i = 0; i < original.getPsbtInputs().size(); i++) {
originalInputs.add(Map.entry(original.getTransaction().getInputs().get(i), original.getPsbtInputs().get(i)));
}
Queue<Map.Entry<TransactionOutput, PSBTOutput>> originalOutputs = new ArrayDeque<>();
for (int i = 0; i < original.getPsbtOutputs().size(); i++) {
originalOutputs.add(Map.entry(original.getTransaction().getOutputs().get(i), original.getPsbtOutputs().get(i)));
}
// Checking that the PSBT of the receiver is clean
if (!proposal.getExtendedPublicKeys().isEmpty()) {
throw new PayjoinReceiverException("Global xpubs should not be included in the receiver's PSBT");
}
Transaction originalTx = original.getTransaction();
Transaction proposalTx = proposal.getTransaction();
// Verify that the transaction version, and nLockTime are unchanged.
if (proposalTx.getVersion() != originalTx.getVersion()) {
throw new PayjoinReceiverException("The proposal PSBT changed the transaction version");
}
if (proposalTx.getLocktime() != originalTx.getLocktime()) {
throw new PayjoinReceiverException("The proposal PSBT changed the nLocktime");
}
Set<Long> sequences = new HashSet<>();
// For each inputs in the proposal:
for (PSBTInput proposedPSBTInput : proposal.getPsbtInputs()) {
if (!proposedPSBTInput.getDerivedPublicKeys().isEmpty()) {
throw new PayjoinReceiverException("The receiver added keypaths to an input");
}
if (!proposedPSBTInput.getPartialSignatures().isEmpty()) {
throw new PayjoinReceiverException("The receiver added partial signatures to an input");
}
TransactionInput proposedTxIn = proposedPSBTInput.getInput();
boolean isOriginalInput = originalInputs.size() > 0 && originalInputs.peek().getKey().getOutpoint().equals(proposedTxIn.getOutpoint());
if (isOriginalInput) {
Map.Entry<TransactionInput, PSBTInput> originalInput = originalInputs.remove();
TransactionInput originalTxIn = originalInput.getKey();
// Verify that sequence is unchanged.
if (originalTxIn.getSequenceNumber() != proposedTxIn.getSequenceNumber()) {
throw new PayjoinReceiverException("The proposed transaction input modified the sequence of one of the original inputs");
}
// Verify the PSBT input is not finalized
if (proposedPSBTInput.isFinalized()) {
throw new PayjoinReceiverException("The receiver finalized one of the original inputs");
}
// Verify that non_witness_utxo and witness_utxo are not specified.
if (proposedPSBTInput.getNonWitnessUtxo() != null || proposedPSBTInput.getWitnessUtxo() != null) {
throw new PayjoinReceiverException("The receiver added non_witness_utxo or witness_utxo to one of the original inputs");
}
sequences.add(proposedTxIn.getSequenceNumber());
PSBTInput originalPSBTInput = originalInput.getValue();
// Fill up the info from the original PSBT input so we can sign and get fees.
proposedPSBTInput.setNonWitnessUtxo(originalPSBTInput.getNonWitnessUtxo());
proposedPSBTInput.setWitnessUtxo(originalPSBTInput.getWitnessUtxo());
// We fill up information we had on the signed PSBT, so we can sign it.
proposedPSBTInput.getDerivedPublicKeys().putAll(originalPSBTInput.getDerivedPublicKeys());
proposedPSBTInput.getProprietary().putAll(originalPSBTInput.getProprietary());
proposedPSBTInput.setRedeemScript(originalPSBTInput.getFinalScriptSig().getFirstNestedScript());
proposedPSBTInput.setWitnessScript(originalPSBTInput.getFinalScriptWitness().getWitnessScript());
proposedPSBTInput.setSigHash(originalPSBTInput.getSigHash());
} else {
// Verify the PSBT input is finalized
if (!proposedPSBTInput.isFinalized()) {
throw new PayjoinReceiverException("The receiver did not finalize one of their inputs");
}
// Verify that non_witness_utxo or witness_utxo are filled in.
if (proposedPSBTInput.getNonWitnessUtxo() == null && proposedPSBTInput.getWitnessUtxo() == null) {
throw new PayjoinReceiverException("The receiver did not specify non_witness_utxo or witness_utxo for one of their inputs");
}
sequences.add(proposedTxIn.getSequenceNumber());
// Verify that the payjoin proposal did not introduced mixed inputs' type.
if (wallet.getScriptType() != proposedPSBTInput.getScriptType()) {
throw new PayjoinReceiverException("Proposal script type of " + proposedPSBTInput.getScriptType() + " did not match wallet script type of " + wallet.getScriptType());
}
}
}
// Verify that all of sender's inputs from the original PSBT are in the proposal.
if (!originalInputs.isEmpty()) {
throw new PayjoinReceiverException("Some of the original inputs are not included in the proposal");
}
// Verify that the payjoin proposal did not introduced mixed inputs' sequence.
if (sequences.size() != 1) {
throw new PayjoinReceiverException("Mixed sequences detected in the proposal");
}
Long newFee = proposal.getFee();
long additionalFee = newFee - original.getFee();
if (additionalFee < 0) {
throw new PayjoinReceiverException("The receiver decreased absolute fee");
}
TransactionOutput changeOutput = (changeOutputIndex > -1 ? originalTx.getOutputs().get(changeOutputIndex) : null);
// For each outputs in the proposal:
for (int i = 0; i < proposal.getPsbtOutputs().size(); i++) {
PSBTOutput proposedPSBTOutput = proposal.getPsbtOutputs().get(i);
// Verify that no keypaths is in the PSBT output
if (!proposedPSBTOutput.getDerivedPublicKeys().isEmpty()) {
throw new PayjoinReceiverException("The receiver added keypaths to an output");
}
TransactionOutput proposedTxOut = proposalTx.getOutputs().get(i);
boolean isOriginalOutput = originalOutputs.size() > 0 && originalOutputs.peek().getKey().getScript().equals(proposedTxOut.getScript());
if (isOriginalOutput) {
Map.Entry<TransactionOutput, PSBTOutput> originalOutput = originalOutputs.remove();
if (originalOutput.getKey() == changeOutput) {
var actualContribution = changeOutput.getValue() - proposedTxOut.getValue();
// The amount that was subtracted from the output's value is less than or equal to maxadditionalfeecontribution
if (actualContribution > maxAdditionalFeeContribution) {
throw new PayjoinReceiverException("The actual contribution is more than maxadditionalfeecontribution");
}
// Make sure the actual contribution is only paying fee
if (actualContribution > additionalFee) {
throw new PayjoinReceiverException("The actual contribution is not only paying fee");
}
// Make sure the actual contribution is only paying for fee incurred by additional inputs
int additionalInputsCount = proposalTx.getInputs().size() - originalTx.getInputs().size();
if (actualContribution > getSingleInputFee() * additionalInputsCount) {
throw new PayjoinReceiverException("The actual contribution is not only paying for additional inputs");
}
} else if (allowOutputSubstitution && originalOutput.getKey().getScript().equals(payjoinURI.getAddress().getOutputScript())) {
// That's the payment output, the receiver may have changed it.
} else {
if (originalOutput.getKey().getValue() > proposedTxOut.getValue()) {
throw new PayjoinReceiverException("The receiver decreased the value of one of the outputs");
}
}
PSBTOutput originalPSBTOutput = originalOutput.getValue();
// We fill up information we had on the signed PSBT, so we can sign it.
proposedPSBTOutput.getDerivedPublicKeys().putAll(originalPSBTOutput.getDerivedPublicKeys());
proposedPSBTOutput.getProprietary().putAll(originalPSBTOutput.getProprietary());
proposedPSBTOutput.setRedeemScript(originalPSBTOutput.getRedeemScript());
proposedPSBTOutput.setWitnessScript(originalPSBTOutput.getWitnessScript());
}
}
// Verify that all of sender's outputs from the original PSBT are in the proposal.
if (!originalOutputs.isEmpty()) {
// The payment output may have been substituted
if (!allowOutputSubstitution || originalOutputs.size() != 1 || !originalOutputs.remove().getKey().getScript().equals(payjoinURI.getAddress().getOutputScript())) {
throw new PayjoinReceiverException("Some of our outputs are not included in the proposal");
}
}
// Add global pubkey map for signing
proposal.getExtendedPublicKeys().putAll(psbt.getExtendedPublicKeys());
proposal.getGlobalProprietary().putAll(psbt.getGlobalProprietary());
}
use of com.sparrowwallet.drongo.psbt.PSBTOutput in project sparrow by sparrowwallet.
the class TransactionController method createOutputTreeItem.
private TreeItem<TransactionForm> createOutputTreeItem(int outputIndex) {
TransactionOutput txOutput = getTransaction().getOutputs().get(outputIndex);
PSBTOutput psbtOutput = null;
if (getPSBT() != null && getPSBT().getPsbtOutputs().size() > txOutput.getIndex()) {
psbtOutput = getPSBT().getPsbtOutputs().get(txOutput.getIndex());
}
OutputForm outputForm = (getPSBT() != null ? new OutputForm(txdata, psbtOutput) : new OutputForm(txdata, txOutput));
return new TreeItem<>(outputForm);
}
use of com.sparrowwallet.drongo.psbt.PSBTOutput in project drongo by sparrowwallet.
the class Wallet method finalise.
public void finalise(PSBT psbt) {
int threshold = getDefaultPolicy().getNumSignaturesRequired();
Map<PSBTInput, WalletNode> signingNodes = getSigningNodes(psbt);
for (int i = 0; i < psbt.getTransaction().getInputs().size(); i++) {
TransactionInput txInput = psbt.getTransaction().getInputs().get(i);
PSBTInput psbtInput = psbt.getPsbtInputs().get(i);
if (psbtInput.isFinalized()) {
continue;
}
WalletNode signingNode = signingNodes.get(psbtInput);
// Transaction parent on PSBT utxo might be null in a witness tx, so get utxo tx hash and utxo index from PSBT tx input
TransactionOutput utxo = new TransactionOutput(null, psbtInput.getUtxo().getValue(), psbtInput.getUtxo().getScript()) {
@Override
public Sha256Hash getHash() {
return txInput.getOutpoint().getHash();
}
@Override
public int getIndex() {
return (int) txInput.getOutpoint().getIndex();
}
};
// TODO: Handle taproot scriptpath spending
int signaturesAvailable = psbtInput.isTaproot() ? (psbtInput.getTapKeyPathSignature() != null ? 1 : 0) : psbtInput.getPartialSignatures().size();
if (signaturesAvailable >= threshold && signingNode != null) {
Transaction transaction = new Transaction();
TransactionInput finalizedTxInput;
if (getPolicyType().equals(PolicyType.SINGLE)) {
ECKey pubKey = signingNode.getPubKey();
TransactionSignature transactionSignature = psbtInput.isTaproot() ? psbtInput.getTapKeyPathSignature() : psbtInput.getPartialSignature(pubKey);
if (transactionSignature == null) {
throw new IllegalArgumentException("Pubkey of partial signature does not match wallet pubkey");
}
finalizedTxInput = signingNode.getWallet().getScriptType().addSpendingInput(transaction, utxo, pubKey, transactionSignature);
} else if (getPolicyType().equals(PolicyType.MULTI)) {
List<ECKey> pubKeys = signingNode.getPubKeys();
Map<ECKey, TransactionSignature> pubKeySignatures = new TreeMap<>(new ECKey.LexicographicECKeyComparator());
for (ECKey pubKey : pubKeys) {
pubKeySignatures.put(pubKey, psbtInput.getPartialSignature(pubKey));
}
List<TransactionSignature> signatures = pubKeySignatures.values().stream().filter(Objects::nonNull).collect(Collectors.toList());
if (signatures.size() < threshold) {
throw new IllegalArgumentException("Pubkeys of partial signatures do not match wallet pubkeys");
}
finalizedTxInput = signingNode.getWallet().getScriptType().addMultisigSpendingInput(transaction, utxo, threshold, pubKeySignatures);
} else {
throw new UnsupportedOperationException("Cannot finalise PSBT for policy type " + getPolicyType());
}
psbtInput.setFinalScriptSig(finalizedTxInput.getScriptSig());
psbtInput.setFinalScriptWitness(finalizedTxInput.getWitness());
psbtInput.clearNonFinalFields();
}
}
psbt.getPsbtOutputs().forEach(PSBTOutput::clearNonFinalFields);
}
Aggregations