Example #1
0
        private static PSBT Sign(Party party, DerivationStrategyBase derivationStrategy, PSBT psbt)
        {
            psbt = psbt.Clone();

            // NBXplorer does not have knowledge of the account key path, KeyPath are private information of each peer
            // NBXplorer only derive 0/* and 1/* on top of provided account xpubs,
            // This mean that the input keypaths in the PSBT are in the form 0/* (as if the account key was the root)
            // RebaseKeyPaths modifies the PSBT by adding the AccountKeyPath in prefix of all the keypaths of the PSBT

            // Note that this is not necessary to do this if the account key is the same as root key.
            // Note that also that you don't have to do this, if you do not pass the account key path in the later SignAll call.
            // however, this is best practice to rebase the PSBT before signing.
            // If you sign with an offline device (hw wallet), the wallet would need the rebased PSBT.
            psbt.RebaseKeyPaths(party.AccountExtPubKey, party.AccountKeyPath);

            Console.WriteLine("A PSBT is a data structure with all information for a wallet to sign.");
            var spend = psbt.GetBalance(derivationStrategy, party.AccountExtPubKey, party.AccountKeyPath);

            Console.WriteLine($"{party.PartyName}, Do you agree to sign this transaction spending {spend}?");
            // Ok I sign
            psbt.SignAll(derivationStrategy,                            // What addresses to derive?
                         party.RootExtKey.Derive(party.AccountKeyPath), // With which account private keys?
                         party.AccountKeyPath);                         // What is the keypath of the account private key. If you did not rebased the keypath like before, you can remove this parameter
            return(psbt);
        }
 private async Task<PSBT> GetPayjoinProposedTX(BitcoinUrlBuilder bip21, PSBT psbt, DerivationSchemeSettings derivationSchemeSettings, BTCPayNetwork btcPayNetwork, CancellationToken cancellationToken)
 {
     var cloned = psbt.Clone();
     cloned = cloned.Finalize();
     await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2.0), cloned.ExtractTransaction(), btcPayNetwork);
     return await _payjoinClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, cancellationToken);
 }
Example #3
0
        public void CanCloneAndCombine(PSBT psbt)
        {
            var tmp = psbt.Clone();

            Assert.Equal(psbt, tmp, ComparerInstance);
            var combined = psbt.Combine(tmp);

            Assert.Equal(psbt, combined, ComparerInstance);
        }
 private uint256 GetExpectedHash(PSBT psbt, Coin[] coins)
 {
     psbt = psbt.Clone();
     psbt.AddCoins(coins);
     if (!psbt.TryGetFinalizedHash(out var hash))
     {
         throw new InvalidOperationException("Unable to get the finalized hash");
     }
     return(hash);
 }
Example #5
0
        private static PSBT CanRoundtripPSBT(PSBT psbt)
        {
            var psbtBefore = psbt.ToString();

            psbt = psbt.Clone();
            var psbtAfter = psbt.ToString();

            Assert.Equal(psbtBefore, psbtAfter);
            return(psbt);
        }
Example #6
0
        private async Task <PSBT> GetPayjoinProposedTX(BitcoinUrlBuilder bip21, PSBT psbt, DerivationSchemeSettings derivationSchemeSettings, BTCPayNetwork btcPayNetwork, CancellationToken cancellationToken)
        {
            var cloned = psbt.Clone();

            cloned = cloned.Finalize();
            await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2.0), cloned.ExtractTransaction(), btcPayNetwork);

            using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            cts.CancelAfter(TimeSpan.FromSeconds(30));
            return(await _payjoinClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, cts.Token));
        }
        private async Task <PSBT> GetPayjoinProposedTX(string bpu, PSBT psbt, DerivationSchemeSettings derivationSchemeSettings, BTCPayNetwork btcPayNetwork, CancellationToken cancellationToken)
        {
            if (string.IsNullOrEmpty(bpu) || !Uri.TryCreate(bpu, UriKind.Absolute, out var endpoint))
            {
                throw new InvalidOperationException("No payjoin url available");
            }
            var cloned = psbt.Clone();

            cloned = cloned.Finalize();
            await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2.0), cloned.ExtractTransaction(), btcPayNetwork);

            return(await _payjoinClient.RequestPayjoin(endpoint, derivationSchemeSettings, psbt, cancellationToken));
        }
        public static async Task <PSBT> SignPsbtWithoutInputTxsAsync(HwiClient client, HDFingerprint value, PSBT psbt, CancellationToken token)
        {
            // Ledger Nano S hackfix https://github.com/MetacoSA/NBitcoin/pull/888

            var noinputtx = psbt.Clone();

            foreach (var input in noinputtx.Inputs)
            {
                input.NonWitnessUtxo = null;
            }

            return(await client.SignTxAsync(value, noinputtx, token).ConfigureAwait(false));
        }
        public async Task <PSBT> SignTransactionAsync(PSBT psbt, Script changeHint,
                                                      CancellationToken cancellationToken)
        {
            try
            {
                var unsigned      = psbt.GetGlobalTransaction();
                var changeKeyPath = psbt.Outputs
                                    .Where(o => changeHint == null ? true : changeHint == o.ScriptPubKey)
                                    .Where(o => o.HDKeyPaths.Any())
                                    .Select(o => o.HDKeyPaths.First().Value.Item2)
                                    .FirstOrDefault();
                var signatureRequests = psbt
                                        .Inputs
                                        .Where(o => o.HDKeyPaths.Any())
                                        .Where(o => !o.PartialSigs.ContainsKey(o.HDKeyPaths.First().Key))
                                        .Select(i => new SignatureRequest()
                {
                    InputCoin        = i.GetSignableCoin(),
                    InputTransaction = i.NonWitnessUtxo,
                    KeyPath          = i.HDKeyPaths.First().Value.Item2,
                    PubKey           = i.HDKeyPaths.First().Key
                }).ToArray();
                var signedTransaction = await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken);

                if (signedTransaction == null)
                {
                    throw new Exception("The ledger failed to sign the transaction");
                }

                psbt = psbt.Clone();
                foreach (var signature in signatureRequests)
                {
                    var input = psbt.Inputs.FindIndexedInput(signature.InputCoin.Outpoint);
                    if (input == null)
                    {
                        continue;
                    }
                    input.PartialSigs.Add(signature.PubKey, signature.Signature);
                }
                return(psbt);
            }
            catch (Exception ex)
            {
                throw new Exception("The ledger failed to sign the transaction", ex);
            }
        }
Example #10
0
        private async Task <PSBT> TryGetPayjoinProposedTX(string bpu, PSBT psbt, DerivationSchemeSettings derivationSchemeSettings, BTCPayNetwork btcPayNetwork, CancellationToken cancellationToken)
        {
            if (!string.IsNullOrEmpty(bpu) && Uri.TryCreate(bpu, UriKind.Absolute, out var endpoint))
            {
                var cloned = psbt.Clone();
                cloned = cloned.Finalize();
                await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(1.0), cloned.ExtractTransaction(), btcPayNetwork);

                try
                {
                    return(await _payjoinClient.RequestPayjoin(endpoint, derivationSchemeSettings, cloned, cancellationToken));
                }
                catch (Exception)
                {
                    return(null);
                }
            }
            return(null);
        }
Example #11
0
        public override async Task <PSBT> SignTransactionAsync(PSBT psbt, HDFingerprint?rootFingerprint, BitcoinExtPubKey accountKey, Script changeHint, CancellationToken cancellationToken)
        {
            var unsigned      = psbt.GetGlobalTransaction();
            var changeKeyPath = psbt.Outputs.HDKeysFor(rootFingerprint, accountKey)
                                .Where(o => changeHint == null ? true : changeHint == o.Coin.ScriptPubKey)
                                .Select(o => o.KeyPath)
                                .FirstOrDefault();
            var signatureRequests = psbt
                                    .Inputs
                                    .HDKeysFor(rootFingerprint, accountKey)
                                    .Where(hd => !hd.Coin.PartialSigs.ContainsKey(hd.PubKey)) // Don't want to sign something twice
                                    .GroupBy(hd => hd.Coin)
                                    .Select(i => new SignatureRequest()
            {
                InputCoin        = i.Key.GetSignableCoin(),
                InputTransaction = i.Key.NonWitnessUtxo,
                KeyPath          = i.First().KeyPath,
                PubKey           = i.First().PubKey
            }).ToArray();
            await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken);

            psbt = psbt.Clone();
            foreach (var signature in signatureRequests)
            {
                if (signature.Signature == null)
                {
                    continue;
                }
                var input = psbt.Inputs.FindIndexedInput(signature.InputCoin.Outpoint);
                if (input == null)
                {
                    continue;
                }
                input.PartialSigs.Add(signature.PubKey, signature.Signature);
            }
            return(psbt);
        }
Example #12
0
        public async Task <PSBT> RequestPayjoin(PSBT originalTx, IHDKey accountKey, RootedKeyPath rootedKeyPath, CancellationToken cancellationToken)
        {
            Guard.NotNull(nameof(originalTx), originalTx);
            if (originalTx.IsAllFinalized())
            {
                throw new InvalidOperationException("The original PSBT should not be finalized.");
            }

            var sentBefore  = -originalTx.GetBalance(ScriptPubKeyType.Segwit, accountKey, rootedKeyPath);
            var oldGlobalTx = originalTx.GetGlobalTransaction();

            if (!originalTx.TryGetEstimatedFeeRate(out var originalFeeRate) || !originalTx.TryGetVirtualSize(out var oldVirtualSize))
            {
                throw new ArgumentException("originalTx should have utxo information", nameof(originalTx));
            }

            var originalFee = originalTx.GetFee();
            var cloned      = originalTx.Clone();

            if (!cloned.TryFinalize(out var _))
            {
                return(null);
            }

            // We make sure we don't send unnecessary information to the receiver
            foreach (var finalized in cloned.Inputs.Where(i => i.IsFinalized()))
            {
                finalized.ClearForFinalize();
            }

            foreach (var output in cloned.Outputs)
            {
                output.HDKeyPaths.Clear();
            }

            cloned.GlobalXPubs.Clear();

            var request = new HttpRequestMessage(HttpMethod.Post, PaymentUrl)
            {
                Content = new StringContent(cloned.ToHex(), Encoding.UTF8, "text/plain")
            };

            HttpResponseMessage bpuResponse = await TorHttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);

            if (!bpuResponse.IsSuccessStatusCode)
            {
                var errorStr = await bpuResponse.Content.ReadAsStringAsync().ConfigureAwait(false);

                try
                {
                    var error = JObject.Parse(errorStr);
                    throw new PayjoinReceiverException((int)bpuResponse.StatusCode,
                                                       error["errorCode"].Value <string>(),
                                                       error["message"].Value <string>());
                }
                catch (JsonReaderException)
                {
                    // will throw
                    bpuResponse.EnsureSuccessStatusCode();
                    throw;
                }
            }

            var hexOrBase64 = await bpuResponse.Content.ReadAsStringAsync().ConfigureAwait(false);

            var newPSBT = PSBT.Parse(hexOrBase64, originalTx.Network);

            // Checking that the PSBT of the receiver is clean
            if (newPSBT.GlobalXPubs.Any())
            {
                throw new PayjoinSenderException("GlobalXPubs should not be included in the receiver's PSBT");
            }

            if (newPSBT.Outputs.Any(o => o.HDKeyPaths.Count != 0) || newPSBT.Inputs.Any(o => o.HDKeyPaths.Count != 0))
            {
                throw new PayjoinSenderException("Keypath information should not be included in the receiver's PSBT");
            }

            if (newPSBT.CheckSanity() is IList <PSBTError> errors2 && errors2.Count != 0)
            {
                throw new PayjoinSenderException($"The PSBT of the receiver is insane ({errors2[0]})");
            }

            // Do not trust on inputs order because the payjoin server should shuffle them.
            foreach (var input in originalTx.Inputs)
            {
                var newInput = newPSBT.Inputs.FindIndexedInput(input.PrevOut);
                if (newInput is { })
Example #13
0
        public async Task <PSBT> RequestPayjoin(Uri endpoint, DerivationSchemeSettings derivationSchemeSettings,
                                                PSBT originalTx, CancellationToken cancellationToken)
        {
            if (endpoint == null)
            {
                throw new ArgumentNullException(nameof(endpoint));
            }
            if (derivationSchemeSettings == null)
            {
                throw new ArgumentNullException(nameof(derivationSchemeSettings));
            }
            if (originalTx == null)
            {
                throw new ArgumentNullException(nameof(originalTx));
            }

            var signingAccount = derivationSchemeSettings.GetSigningAccountKeySettings();
            var sentBefore     = -originalTx.GetBalance(derivationSchemeSettings.AccountDerivation,
                                                        signingAccount.AccountKey,
                                                        signingAccount.GetRootedKeyPath());
            var oldGlobalTx = originalTx.GetGlobalTransaction();

            if (!originalTx.TryGetEstimatedFeeRate(out var originalFeeRate) || !originalTx.TryGetVirtualSize(out var oldVirtualSize))
            {
                throw new ArgumentException("originalTx should have utxo information", nameof(originalTx));
            }
            var originalFee = originalTx.GetFee();
            var cloned      = originalTx.Clone();

            if (!cloned.IsAllFinalized() && !cloned.TryFinalize(out var errors))
            {
                return(null);
            }

            // We make sure we don't send unnecessary information to the receiver
            foreach (var finalized in cloned.Inputs.Where(i => i.IsFinalized()))
            {
                finalized.ClearForFinalize();
            }

            foreach (var output in cloned.Outputs)
            {
                output.HDKeyPaths.Clear();
            }

            cloned.GlobalXPubs.Clear();
            var bpuresponse = await _httpClient.PostAsync(endpoint,
                                                          new StringContent(cloned.ToHex(), Encoding.UTF8, "text/plain"), cancellationToken);

            if (!bpuresponse.IsSuccessStatusCode)
            {
                var errorStr = await bpuresponse.Content.ReadAsStringAsync();

                try
                {
                    var error = JObject.Parse(errorStr);
                    throw new PayjoinReceiverException((int)bpuresponse.StatusCode, error["errorCode"].Value <string>(),
                                                       error["message"].Value <string>());
                }
                catch (JsonReaderException)
                {
                    // will throw
                    bpuresponse.EnsureSuccessStatusCode();
                    throw;
                }
            }

            var hex = await bpuresponse.Content.ReadAsStringAsync();

            var newPSBT = PSBT.Parse(hex, originalTx.Network);

            // Checking that the PSBT of the receiver is clean
            if (newPSBT.GlobalXPubs.Any())
            {
                throw new PayjoinSenderException("GlobalXPubs should not be included in the receiver's PSBT");
            }

            if (newPSBT.Outputs.Any(o => o.HDKeyPaths.Count != 0) || newPSBT.Inputs.Any(o => o.HDKeyPaths.Count != 0))
            {
                throw new PayjoinSenderException("Keypath information should not be included in the receiver's PSBT");
            }
            ////////////

            newPSBT = await _explorerClientProvider.UpdatePSBT(derivationSchemeSettings, newPSBT);

            if (newPSBT.CheckSanity() is IList <PSBTError> errors2 && errors2.Count != 0)
            {
                throw new PayjoinSenderException($"The PSBT of the receiver is insane ({errors2[0]})");
            }
            // We make sure we don't sign things what should not be signed
            foreach (var finalized in newPSBT.Inputs.Where(i => i.IsFinalized()))
            {
                finalized.ClearForFinalize();
            }
            // Make sure only the only our output have any information
            foreach (var output in newPSBT.Outputs)
            {
                output.HDKeyPaths.Clear();
                foreach (var originalOutput in  originalTx.Outputs)
                {
                    if (output.ScriptPubKey == originalOutput.ScriptPubKey)
                    {
                        output.UpdateFrom(originalOutput);
                    }
                }
            }

            // Making sure that our inputs are finalized, and that some of our inputs have not been added
            var newGlobalTx   = newPSBT.GetGlobalTransaction();
            int ourInputCount = 0;

            if (newGlobalTx.Version != oldGlobalTx.Version)
            {
                throw new PayjoinSenderException("The version field of the transaction has been modified");
            }
            if (newGlobalTx.LockTime != oldGlobalTx.LockTime)
            {
                throw new PayjoinSenderException("The LockTime field of the transaction has been modified");
            }
            foreach (var input in newPSBT.Inputs.CoinsFor(derivationSchemeSettings.AccountDerivation,
                                                          signingAccount.AccountKey, signingAccount.GetRootedKeyPath()))
            {
                if (originalTx.Inputs.FindIndexedInput(input.PrevOut) is PSBTInput ourInput)
                {
                    ourInputCount++;
                    if (input.IsFinalized())
                    {
                        throw new PayjoinSenderException("A PSBT input from us should not be finalized");
                    }
                    if (newGlobalTx.Inputs[input.Index].Sequence != newGlobalTx.Inputs[ourInput.Index].Sequence)
                    {
                        throw new PayjoinSenderException("The sequence of one of our input has been modified");
                    }
                }
                else
                {
                    throw new PayjoinSenderException(
                              "The payjoin receiver added some of our own inputs in the proposal");
                }
            }

            // Making sure that the receiver's inputs are finalized and P2PWKH
            foreach (var input in newPSBT.Inputs)
            {
                if (originalTx.Inputs.FindIndexedInput(input.PrevOut) is null)
                {
                    if (!input.IsFinalized())
                    {
                        throw new PayjoinSenderException("The payjoin receiver included a non finalized input");
                    }
                    if (!(input.FinalScriptWitness.GetSigner() is WitKeyId))
                    {
                        throw new PayjoinSenderException("The payjoin receiver included an input that is not P2PWKH");
                    }
                }
            }

            if (ourInputCount < originalTx.Inputs.Count)
            {
                throw new PayjoinSenderException("The payjoin receiver removed some of our inputs");
            }

            // We limit the number of inputs the receiver can add
            var addedInputs = newPSBT.Inputs.Count - originalTx.Inputs.Count;

            if (addedInputs == 0)
            {
                throw new PayjoinSenderException("The payjoin receiver did not added any input");
            }

            var sentAfter = -newPSBT.GetBalance(derivationSchemeSettings.AccountDerivation,
                                                signingAccount.AccountKey,
                                                signingAccount.GetRootedKeyPath());

            if (sentAfter > sentBefore)
            {
                var overPaying = sentAfter - sentBefore;
                if (!newPSBT.TryGetEstimatedFeeRate(out var newFeeRate) || !newPSBT.TryGetVirtualSize(out var newVirtualSize))
                {
                    throw new PayjoinSenderException("The payjoin receiver did not included UTXO information to calculate fee correctly");
                }
                var additionalFee = newPSBT.GetFee() - originalFee;
                if (overPaying > additionalFee)
                {
                    throw new PayjoinSenderException("The payjoin receiver is sending more money to himself");
                }
                if (overPaying > originalFee)
                {
                    throw new PayjoinSenderException("The payjoin receiver is making us pay more than twice the original fee");
                }

                // Let's check the difference is only for the fee and that feerate
                // did not changed that much
                var expectedFee = originalFeeRate.GetFee(newVirtualSize);
                // Signing precisely is hard science, give some breathing room for error.
                expectedFee += originalFeeRate.GetFee(newPSBT.Inputs.Count * 2);
                if (overPaying > (expectedFee - originalFee))
                {
                    throw new PayjoinSenderException("The payjoin receiver increased the fee rate we are paying too much");
                }
            }

            return(newPSBT);
        }