Search in sources :

Example 1 with PSBTOutput

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);
    }
}
Also used : PSBTOutput(com.sparrowwallet.drongo.psbt.PSBTOutput) PSBTInput(com.sparrowwallet.drongo.psbt.PSBTInput)

Example 2 with PSBTOutput

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);
    }
}
Also used : PSBTOutput(com.sparrowwallet.drongo.psbt.PSBTOutput) PSBTEntry(com.sparrowwallet.drongo.psbt.PSBTEntry)

Example 3 with PSBTOutput

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());
}
Also used : TransactionOutput(com.sparrowwallet.drongo.protocol.TransactionOutput) PSBTOutput(com.sparrowwallet.drongo.psbt.PSBTOutput) TransactionInput(com.sparrowwallet.drongo.protocol.TransactionInput) Transaction(com.sparrowwallet.drongo.protocol.Transaction) PSBTInput(com.sparrowwallet.drongo.psbt.PSBTInput) ImmutableMap(com.google.common.collect.ImmutableMap)

Example 4 with PSBTOutput

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);
}
Also used : PSBTOutput(com.sparrowwallet.drongo.psbt.PSBTOutput) TreeItem(javafx.scene.control.TreeItem)

Example 5 with PSBTOutput

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);
}
Also used : PSBTOutput(com.sparrowwallet.drongo.psbt.PSBTOutput) ECKey(com.sparrowwallet.drongo.crypto.ECKey) PSBTInput(com.sparrowwallet.drongo.psbt.PSBTInput)

Aggregations

PSBTOutput (com.sparrowwallet.drongo.psbt.PSBTOutput)7 PSBTInput (com.sparrowwallet.drongo.psbt.PSBTInput)5 PSBTEntry (com.sparrowwallet.drongo.psbt.PSBTEntry)2 ImmutableMap (com.google.common.collect.ImmutableMap)1 ECKey (com.sparrowwallet.drongo.crypto.ECKey)1 Transaction (com.sparrowwallet.drongo.protocol.Transaction)1 TransactionInput (com.sparrowwallet.drongo.protocol.TransactionInput)1 TransactionOutput (com.sparrowwallet.drongo.protocol.TransactionOutput)1 TreeItem (javafx.scene.control.TreeItem)1