sparrowwallet / sparrow

Desktop Bitcoin Wallet focused on security and privacy. Free and open source.
https://sparrowwallet.com/
Apache License 2.0
1.28k stars 181 forks source link

Attempting PayJoin via PSBT broadcasts transaction w/o user consent #1467

Closed sethforprivacy closed 1 month ago

sethforprivacy commented 1 month ago

I was attempting to pay a PayJoin on a BTCPay server, and when I tried to get the PayJoin input after signing the initial PSBT I got this error:

image

That would be fine except that the transaction was still broadcast silently without my consent, and without being PayJoined. The BTCPay invoice being marked as paid immediately after the error was my first clue, but I couldn't find the TX in mempool at first. Eventually Sparrow finally showed the transaction (with notification) while I still hadn't hit the Broadcast Transaction button. My guess is that when I tried to get the PayJoin input Sparrow for some reason broadcast the transaction to the network, OR when sending the signed PSBT to BTCPay, BTCPay broadcast the transaction for some reason. I can't share the transaction details here, but happy to forward them in DMs to you if useful, @craigraw.

sethforprivacy commented 1 month ago

P.S. - it's most likely this is a broken interaction that involves both better error-handling on Sparrow's side and some issues with how BTCPay handles PayJoins. Happy to open an issue there once we dig a little deeper.

craigraw commented 1 month ago

Yes, this is often reported by new users of this functionality. BTCPayServer always extracts and broadcasts the transaction from the original PSBT after a certain number of seconds. The only way to stop this is to successfully complete the payjoin, in which case the payjoin PSBT can be used. I understand the motivation - the merchant always gets paid - but for a user it's often unexpected which is not ideal.

Unfortunately, there's nothing Sparrow can do about it within the constraints of the Payjoin spec.

sethforprivacy commented 1 month ago

Is there any way to tell why it failed? The error was less than descriptive and would be good to be sure there isn't anything broken in the PayJoin comms. The transaction was broadcast immediately (by BTCPay it seems) when I tried to get the PayJoin input and got this error, there was not even a delay.

craigraw commented 1 month ago

Is there any way to tell why it failed? The error was less than descriptive and would be good to be sure there isn't anything broken in the PayJoin comms.

The BIP78 spec defines four well known error codes: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#receivers-well-known-errors. In order to avoid displaying a message from a potential attacker, Sparrow will not display any other error messages within the UI, and defaults to Unknown Error. You can check the log file (Help menu) for a more detailed error message.

The transaction was broadcast immediately (by BTCPay it seems) when I tried to get the PayJoin input and got this error, there was not even a delay.

It shouldn't be immediate, otherwise there would be no way to ever complete a payjoin. But it's not long - in the order of seconds or a minute as I recall.

sethforprivacy commented 1 month ago

Ah, interesting, it was a script-type issue:

2024-07-22 07:16:36,117 ERROR [Thread-60] c.s.s.p.Payjoin [null:-1] Payjoin error
com.sparrowwallet.sparrow.payjoin.PayjoinReceiverException: Proposal script type of P2WPKH did not match wallet script type of P2TR
    at com.sparrowwallet.sparrow@1.9.1/com.sparrowwallet.sparrow.payjoin.Payjoin.checkProposal(Unknown Source)
    at com.sparrowwallet.sparrow@1.9.1/com.sparrowwallet.sparrow.payjoin.Payjoin.requestPayjoinPSBT(Unknown Source)
    at com.sparrowwallet.sparrow@1.9.1/com.sparrowwallet.sparrow.payjoin.Payjoin$RequestPayjoinPSBTService$1.call(Unknown Source)
    at com.sparrowwallet.sparrow@1.9.1/com.sparrowwallet.sparrow.payjoin.Payjoin$RequestPayjoinPSBTService$1.call(Unknown Source)
    at javafx.graphics@18/javafx.concurrent.Task$TaskCallable.call(Unknown Source)
    at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
    at javafx.graphics@18/javafx.concurrent.Service.lambda$executeTask$6(Unknown Source)
    at java.base/java.security.AccessController.doPrivileged(Unknown Source)
    at javafx.graphics@18/javafx.concurrent.Service.lambda$executeTask$7(Unknown Source)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
    at java.base/java.lang.Thread.run(Unknown Source)
2024-07-22 07:16:39,904 ERROR [RxCachedThreadScheduler-1] c.s.s.n.h.c.JettyHttpClient [null:-1] Http query failed: status=422, responseBody={"errorCode":"already-paid","message":"The invoice this PSBT is paying has already been partially or completely paid"}
2024-07-22 07:16:39,908 WARN [Thread-63] c.s.s.p.Payjoin [null:-1] Payjoin receiver returned an error of already-paid (The invoice this PSBT is paying has already been partially or completely paid)
2024-07-22 07:17:04,263 ERROR [RxCachedThreadScheduler-1] c.s.s.n.h.c.JettyHttpClient [null:-1] Http query failed: status=422, responseBody={"errorCode":"already-paid","message":"The invoice this PSBT is paying has already been partially or completely paid"}
2024-07-22 07:17:04,263 WARN [Thread-65] c.s.s.p.Payjoin [null:-1] Payjoin receiver returned an error of already-paid (The invoice this PSBT is paying has already been partially or completely paid)

Seems that the BTCPay server broadcasted the transaction ~3s after the initial error.

craigraw commented 1 month ago

That seems pretty quick - might be worth inquiring with them about such a short time period.

Should we close this off?

DanGould commented 1 month ago

It'd be helpful if you can figure out what error BTCPayServer threw, can you? If it were possible to Payjoin, sparrow should show "The receiver rejected the original PSBT." The unknown error suggests that something else happened here. My guess is the BTCPayServer had no coin to select.

I think the /real/ error message should show up in Sparrow debug logs if such a thing is available.