From 3485af708ce5a485d30cd8f60efe0004e9b4566c Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Wed, 17 Jun 2020 22:26:03 +0900 Subject: Add reference implementation --- bip-0078.mediawiki | 232 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) (limited to 'bip-0078.mediawiki') diff --git a/bip-0078.mediawiki b/bip-0078.mediawiki index ddde613..70adf1c 100644 --- a/bip-0078.mediawiki +++ b/bip-0078.mediawiki @@ -381,6 +381,238 @@ Without payjoin, the maximum amount of money that could be lost by a compromised With payjoin, the maximum amount of money that can be lost is equal to two payments. +==Reference sender's implementation== + +Here is pseudo code of a sender implementation. +RequestPayjoin takes the bip21 URI of the payment, the wallet and the signedPSBT. + +The signedPSBT represents a PSBT which has been fully signed, but not yet finalized. +We then prepare originalPSBT from the signedPSBT via the CreateOriginalPSBT function and get back the proposal. + +While we verify the proposal, we also import into it informations about our own inputs and outputs from the signedPSBT. +At the end of this RequestPayjoin, the proposal is verified and ready to be signed. + +We logged the different PSBT involved, and show the result in our [[#test-vectors|test vectors]]. +
+public async Task RequestPayjoin(
+    BIP21Uri bip21,
+    Wallet wallet,
+    PSBT signedPSBT,
+    PayjoinClientParameters optionalParameters)
+{
+    Log("signed PSBT" + signedPSBT);
+    var endpoint = bip21.ExtractPayjointEndpoint();
+    if (signedPSBT.IsAllFinalized())
+        throw new InvalidOperationException("The original PSBT should not be finalized.");
+    ScriptPubKeyType inputScriptType = wallet.ScriptPubKeyType();
+    PSBTOutput feePSBTOutput = null;
+    if (optionalParameters.AdditionalFeeOutputIndex != null && optionalParameters.MaxAdditionalFeeContribution != null)
+        feePSBTOutput = signedPSBT.Outputs[optionalParameters.AdditionalFeeOutputIndex];
+    decimal originalFee = signedPSBT.GetFee();
+    PSBT originalPSBT = CreateOriginalPSBT(signedPSBT);
+    Transaction originalGlobalTx = signedPSBT.GetGlobalTransaction();
+    TxOut feeOutput = feePSBTOutput == null ? null : originalGlobalTx.Outputs[feePSBTOutput.Index];
+    var ourInputs = new Queue<(TxIn OriginalTxIn, PSBTInput SignedPSBTInput)>();
+    for (int i = 0; i < originalGlobalTx.Inputs.Count; i++)
+    {
+        ourInputs.Enqueue((originalGlobalTx.Inputs[i], signedPSBT.Inputs[i]));
+    }
+    var ourOutputs = new Queue<(TxOut OriginalTxOut, PSBTOutput SignedPSBTOutput)>();
+    for (int i = 0; i < originalGlobalTx.Outputs.Count; i++)
+    {
+        if (signedPSBT.Outputs[i].ScriptPubKey != bip21.Address.ScriptPubKey)
+            ourOutputs.Enqueue((originalGlobalTx.Outputs[i], signedPSBT.Outputs[i]));
+    }
+    endpoint = ApplyOptionalParameters(endpoint, optionalParameters);
+    Log("original PSBT" + originalPSBT);
+    PSBT proposal = await SendOriginalTransaction(endpoint, originalPSBT, cancellationToken);
+    Log("payjoin proposal" + proposal);
+    // Checking that the PSBT of the receiver is clean
+    if (proposal.GlobalXPubs.Any())
+    {
+        throw new PayjoinSenderException("GlobalXPubs should not be included in the receiver's PSBT");
+    }
+    ////////////
+
+    if (proposal.CheckSanity() is List errors && errors.Count > 0)
+        throw new PayjoinSenderException($"The proposal PSBT is not insance ({errors[0]})");
+
+    var proposalGlobalTx = proposal.GetGlobalTransaction();
+    // Verify that the transaction version, and nLockTime are unchanged.
+    if (proposalGlobalTx.Version != originalGlobalTx.Version)
+        throw new PayjoinSenderException($"The proposal PSBT changed the transaction version");
+    if (proposalGlobalTx.LockTime != originalGlobalTx.LockTime)
+        throw new PayjoinSenderException($"The proposal PSBT changed the nLocktime");
+
+    HashSet sequences = new HashSet();
+    // For each inputs in the proposal:
+    foreach (PSBTInput proposedPSBTInput in proposal.Inputs)
+    {
+        if (proposedPSBTInput.HDKeyPaths.Count != 0)
+            throw new PayjoinSenderException("The receiver added keypaths to an input");
+        if (proposedPSBTInput.PartialSigs.Count != 0)
+            throw new PayjoinSenderException("The receiver added partial signatures to an input");
+        PSBTInput proposedTxIn = proposalGlobalTx.Inputs.FindIndexedInput(proposedPSBTInput.PrevOut).TxIn;
+        bool isOurInput = ourInputs.Count > 0 && ourInputs.Peek().OriginalTxIn.PrevOut == proposedPSBTInput.PrevOut;
+        // If it is one of our input
+        if (isOurInput)
+        {
+            OutPoint inputPrevout = ourPrevouts.Dequeue();
+            TxIn originalTxin = originalGlobalTx.Inputs.FromOutpoint(inputPrevout);
+            PSBTInput originalPSBTInput = originalPSBT.Inputs.FromOutpoint(inputPrevout);
+            // Verify that sequence is unchanged.
+            if (input.OriginalTxIn.Sequence != proposedTxIn.Sequence)
+                throw new PayjoinSenderException("The proposedTxIn modified the sequence of one of our inputs")
+            // Verify the PSBT input is not finalized
+            if (proposedPSBTInput.IsFinalized())
+                throw new PayjoinSenderException("The receiver finalized one of our inputs");
+            // Verify that non_witness_utxo and witness_utxo are not specified.
+            if (proposedPSBTInput.NonWitnessUtxo != null || proposedPSBTInput.WitnessUtxo != null)
+                throw new PayjoinSenderException("The receiver added non_witness_utxo or witness_utxo to one of our inputs");
+            sequences.Add(proposedTxIn.Sequence);
+            
+            // Fill up the info from the original PSBT input so we can sign and get fees.
+            proposedPSBTInput.NonWitnessUtxo = input.SignedPSBTInput.NonWitnessUtxo;
+            proposedPSBTInput.WitnessUtxo = input.SignedPSBTInput.WitnessUtxo;
+            // We fill up information we had on the signed PSBT, so we can sign it.
+            foreach (var hdKey in input.SignedPSBTInput.HDKeyPaths)
+            proposedPSBTInput.HDKeyPaths.Add(hdKey.Key, hdKey.Value);
+            proposedPSBTInput.RedeemScript = signedPSBTInput.RedeemScript;
+            proposedPSBTInput.RedeemScript = input.SignedPSBTInput.RedeemScript;
+        }
+        else
+        {
+            // Verify the PSBT input is finalized
+            if (!proposedPSBTInput.IsFinalized())
+                throw new PayjoinSenderException("The receiver did not finalized one of their input");
+            // Verify that non_witness_utxo or witness_utxo are filled in.
+            if (proposedPSBTInput.NonWitnessUtxo == null && proposedPSBTInput.WitnessUtxo == null)
+                throw new PayjoinSenderException("The receiver did not specify non_witness_utxo or witness_utxo for one of their inputs");
+            sequences.Add(proposedTxIn.Sequence);
+            // Verify that the payjoin proposal did not introduced mixed input's type.
+            if (inputScriptType != proposedPSBTInput.GetInputScriptPubKeyType())
+                throw new PayjoinSenderException("Mixed input type detected in the proposal");
+        }
+    }
+
+    // Verify that all of sender's inputs from the original PSBT are in the proposal.
+    if (ourInputs.Count != 0)
+        throw new PayjoinSenderException("Some of our inputs are not included in the proposal");
+
+    // Verify that the payjoin proposal did not introduced mixed input's sequence.
+    if (sequences.Count != 1)
+        throw new PayjoinSenderException("Mixed sequence detected in the proposal");
+
+    // For each outputs in the proposal:
+    foreach (PSBTOutput proposedPSBTOutput in proposal.Outputs)
+    {
+        // Verify that no keypaths is in the PSBT output
+        if (proposedPSBTOutput.HDKeyPaths.Count != 0)
+            throw new PayjoinSenderException("The receiver added keypaths to an output");
+        bool isOurOutput = ourOutputs.Count > 0 && ourOutputs.Peek().OriginalTxOut.ScriptPubKey == proposedPSBTOutput.ScriptPubKey;
+        if (isOurOutput)
+        {
+            var output = ourOutputs.Dequeue();
+            if (output.OriginalTxOut == feeOutput)
+            {
+                var actualContribution = feeOutput.Value - proposedPSBTOutput.Value;
+                // The amount that was substracted from the output's value is less or equal to maxadditionalfeecontribution
+                if (actualContribution > optionalParameters.MaxAdditionalFeeContribution)
+                    throw new PayjoinSenderException("The actual contribution is more than maxadditionalfeecontribution");
+                decimal newFee = proposal.GetFee();
+                decimal additionalFee = newFee - originalFee;
+                // Make sure the actual contribution is only paying fee
+                if (actualContribution > additionalFee)
+                    throw new PayjoinSenderException("The actual contribution is not only paying fee");
+                // Make sure the actual contribution is only paying for fee incurred by additional inputs
+                int additionalInputsCount = proposalGlobalTx.Inputs.Count - originalGlobalTx.Inputs.Count;
+                if (actualContribution > originalFeeRate * GetVirtualSize(inputScriptType) * additionalInputsCount)
+                    throw new PayjoinSenderException("The actual contribution is not only paying for additional inputs");
+            }
+            else
+            {
+                if (output.OriginalTxOut.Value != proposedPSBTOutput.Value)
+                    throw new PayjoinSenderException("The receiver changed one of our outputs");
+            }
+            // We fill up information we had on the signed PSBT, so we can sign it.
+            foreach (var hdKey in output.SignedPSBTOutput.HDKeyPaths)
+                proposedPSBTOutput.HDKeyPaths.Add(hdKey.Key, hdKey.Value);
+            proposedPSBTOutput.RedeemScript = output.SignedPSBTOutput.RedeemScript;
+        }
+    }
+    // Verify that all of sender's outputs from the original PSBT are in the proposal.
+    if (ourOutputs.Count != 0)
+        throw new PayjoinSenderException("Some of our outputs are not included in the proposal");
+
+    // After signing this proposal, we should check if minfeerate is respected.
+    Log("payjoin proposal filled with sender's information" + proposal);
+    return proposal;
+}
+
+int GetVirtualSize(ScriptPubKeyType? scriptPubKeyType)
+{
+    switch (scriptPubKeyType)
+    {
+        case ScriptPubKeyType.Legacy:
+            return 148;
+        case ScriptPubKeyType.Segwit:
+            return 68;
+        case ScriptPubKeyType.SegwitP2SH:
+            return 91;
+        default:
+            return 110;
+    }
+}
+
+// Finalized the signedPSBT and remove confidential information
+PSBT CreateOriginalPSBT(PSBT signedPSBT)
+{
+    var original = signedPSBT.Clone();
+    original = original.Finalize();
+    foreach (var input in original.Inputs)
+    {
+        input.HDKeyPaths.Clear();
+        input.PartialSigs.Clear();
+        input.Unknown.Clear();
+    }
+    foreach (var output in original.Outputs)
+    {
+        output.Unknown.Clear();
+        output.HDKeyPaths.Clear();
+    }
+    original.GlobalXPubs.Clear();
+    return original;
+}
+
+ +==Test vectors== + +A successful exchange with: + +{| class="wikitable" +!InputScriptType +!Orginal PSBT Fee rate +!maxadditionalfeecontribution +!additionalfeeoutputindex +|- +|P2SH-P2WSH +|2 sat/vbyte +|0.00000182 +|0 +|} + +signed PSBT +
cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQQWABTHikVyU1WCjVZYB03VJg1fy2mFMCICAxWawBqg1YdUxLTYt9NJ7R7fzws2K09rVRBnI6KFj4UWRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIgYDFZrAGqDVh1TEtNi300ntHt/PCzYrT2tVEGcjooWPhRYYSFzWUDEAAIABAACAAAAAgAEAAAAAAAAAAAEAFgAURvYaK7pzgo7lhbSl/DeUan2MxRQiAgLKC8FYHmmul/HrXLUcMDCjfuRg/dhEkG8CO26cEC6vfBhIXNZQMQAAgAEAAIAAAACAAQAAAAEAAAAAAA==
+ +original PSBT +
cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA=
+ +payjoin proposal +
cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAAEBIICEHgAAAAAAF6kUyPLL+cphRyyI5GTUazV0hF2R2NWHAQcXFgAUX4BmVeWSTJIEwtUb5TlPS/ntohABCGsCRzBEAiBnu3tA3yWlT0WBClsXXS9j69Bt+waCs9JcjWtNjtv7VgIge2VYAaBeLPDB6HGFlpqOENXMldsJezF9Gs5amvDQRDQBIQJl1jz1tBt8hNx2owTm+4Du4isx0pmdKNMNIjjaMHFfrQAAAA==
+ +payjoin proposal filled with sender's information +
cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAQEgqBvXBQAAAAAXqRTeTh6QYcpZE1sDWtXm1HmQRUNU0IcBBBYAFMeKRXJTVYKNVlgHTdUmDV/LaYUwIgYDFZrAGqDVh1TEtNi300ntHt/PCzYrT2tVEGcjooWPhRYYSFzWUDEAAIABAACAAAAAgAEAAAAAAAAAAAEBIICEHgAAAAAAF6kUyPLL+cphRyyI5GTUazV0hF2R2NWHAQcXFgAUX4BmVeWSTJIEwtUb5TlPS/ntohABCGsCRzBEAiBnu3tA3yWlT0WBClsXXS9j69Bt+waCs9JcjWtNjtv7VgIge2VYAaBeLPDB6HGFlpqOENXMldsJezF9Gs5amvDQRDQBIQJl1jz1tBt8hNx2owTm+4Du4isx0pmdKNMNIjjaMHFfrQABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUIgICygvBWB5prpfx61y1HDAwo37kYP3YRJBvAjtunBAur3wYSFzWUDEAAIABAACAAAAAgAEAAAABAAAAAAA=
+ ==Implementations== * [[https://github.com/BlueWallet/BlueWallet|BlueWallet]] is in the process of implementing the protocol. -- cgit v1.2.3