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); }
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); }
private static PSBT CanRoundtripPSBT(PSBT psbt) { var psbtBefore = psbt.ToString(); psbt = psbt.Clone(); var psbtAfter = psbt.ToString(); Assert.Equal(psbtBefore, psbtAfter); 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); 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); } }
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); }
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); }
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 { })
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); }