Decoding a BIP-70 Payment Request
I was trying to understand BIP-70 Payment Requests a bit better, mainly because I am confused by BitPay’s claim that they can somehow block “mistaken” transactions:
We can also analyze transactions to make sure an adequate bitcoin miner fee is included. If the fee isn’t sufficient to allow the transaction to confirm on the bitcoin network on time, BitPay can return a helpful message back to the wallet to let the user know. Mistaken payments will never reach the Bitcoin network.
The wording suggests that a wallet sends the transaction to BitPay for approval and they forward it, but afaik that’s not what the BIP-70 does. According to the specification wallets broadcast the transaction via the P2P network like any other transaction. Of course BitPay can always choose to not honor a transaction they receive, but I don’t see how BIP-70 changes that.
In fact, it would be quite unsafe if it did work that way. Other than reputation, what’s to prevent BitPay from rejecting a transaction, telling the customer to submit a new one and then broadcasting both to the network? The wallet would have to be very careful to prevent this; it would have to reuse at least one input in each attempt. Or it could propose unsigned transactions. Or, combined with Replace-By-Fee, perhaps users could send BitPay a series of overlapping transactions with escalating fees, and they would then broadcast the higher fee ones if the confirmation deadline is approached. However that’s totally beyond what BIP-70 is about.
Perhaps they meant that BIP-70 makes it less likely for a user to pay the wrong fee? However the specification doesn’t have a field for a (suggested) fee. Neither does the simpler and more commonly used BIP-21.
Even if there was an ad-hoc method to suggest a fee, at least Bitcoin Core doesn’t honor that. Maybe other wallets do?
Time to look under the hood.
Whenever I get confused, I prefer to just look at what actual software does, rather than speculate based on what people write on blogs or even what a spec says.
BitPay has a demo page where you can generate a payment request. You can use software like QR Journal on MacOS to see what’s in the QR code:
The URI starts with bitcoin:
which is defined in BIP-21. Both browsers and mobile apps use URIs like this to determine which application to open. In the case of bitcoin:
that’s usually whichever Bitcoin wallet you installed last. This is similar to how mailto:someone@example.com?subject=Hello
opens your mail app (even if it’s web based) and creates a draft email with the right address and subject.
A typical BIP-21 URI would contain the destination and amount, e.g. bitcoin:3AcqBykYEos8EHREr7oEzSxg7DxxjH6DCf?amount=0.0001
In this case, the first and only argument is r=
which is defined in BIP-72 as an extention of BIP-21 to indicate a URL where further details can be fetched. This saves space compared to putting all the details directly in the QR code, although I wonder if the more QR friendly bech32 could mitigate that.
If you open that URL in a browser, it will show you the invoice page. But a wallet will pass a special HTTP header to tell the server it wants the actual payment request:
wget --header="Accept: application/bitcoin-paymentrequest" https://bitpay.com/i/8JcqpMf87GJWpma6mRCPGD
The result is a protocol buffer; you can tell Mike Hearn, one of the BIP-70 authors, worked at Google before. 🙂 There’s various ways to decode those, though none are properly documented in the BIP.
The first step is to download the Payment Request protocol buffer description file. The paymentrequest.proto file linked to in the BIP doesn’t specify the protocol buffer version, so I’m using paymentrequest.proto from Bitcoin Core.
You can recognise the above high level structure with a simple command (I replaced long binary stuff with…
):
protoc --decode payments.PaymentRequest paymentrequest.proto < 8JcqpMf87GJWpma6mRCPGD
payment_details_version: 1 pki_type: "x509+sha256" pki_data: "\n\263\0160\202\007/...\007Georgia1\0200\016\006\003U\004\007\023\007Atlanta1\0250\023\006\003U\004\n\023\014BitPay, Inc.1\0230\....\026+http://certificates.godaddy.com/repository/0\007\006\206!http://crl.godaddy.com/gdroot.crl0F\006\003U\035 \004?0=0;\006\004U\..." serialized_payment_details: "\n\004main\022\037\010\...*NPayment request for BitPay invoice 8JcqpMf87GJWpma6mRCPGD for merchant BitGive2+https://bitpay.com/i/8JcqpMf87GJWpma6mRCPGD:L{\"invoiceId\":\"8JcqpMf87GJWpma6mRCPGD\",\"merchantId\":\"TxZ5RyChmZw2isKjJWGhBc\"}" signature: "\\\231\225\216..."
The interesting bit is serialized_payment_details
, which isn’t decoded here. It’s defined as:
So let’s use a better tool. First we convert paymentrequest.proto
to something Python understands:
pip install protobuf protoc --python_out=. paymentrequest.proto
We can then load the invoice and inspect it:
Notice that indeed no fee is specified. However they do increase the amount to offset fees they need to pay to sweep it. This raises some other questions such as why they don’t just give you the the address that they utlimately want to forward it to (and would they have the same policy, and so forth, meaning fees should be infinite?). Also, what time horizon do they have in mind for that sweep? If they’re not in a hurry, or wait for signature aggregation techniques to become available, those fees might be much lower. Anyway, this has no bearing on BIP-70.
I think the confusion arises around the Payment message:
This is sent by the wallet to the merchant before the wallet broadcast the payment via the P2P protocol, but there’s nothing in the spec that says the wallet needs to wait for approval (and see above for why this would be risky with signed transactions).
Note that in the description these steps are reversed, but it doesn’t really matter:
BitPay can inspect the Payment
message and refuse to send a PaymentACK
, but that’s too late. That said, perhaps it’s not actually implemented that way in some wallets; I haven’t checked. For that I’d need to figure out how to intercept the message Bitcoin Core sends (or study the code). Maybe I’ll update this post later.