Skip to content

Instantly share code, notes, and snippets.

@etherx-dev
Forked from AdamISZ/P2EP-for-JM.md
Created February 15, 2021 03:45
Show Gist options
  • Save etherx-dev/70ce438be416b5af418e362ed23d9a95 to your computer and use it in GitHub Desktop.
Save etherx-dev/70ce438be416b5af418e362ed23d9a95 to your computer and use it in GitHub Desktop.
Basic payjoin/p2ep protocol for Joinmarket wallets

Described here is a variant of what has previously been published under the name "P2EP" or Pay-to-endpoint, in which A pays B but B contributes utxos, i.e. it's a coinjoin-payment.

I'm using the term "payjoin" here to refer to using that idea, but not including a URI/endpoint specific to B, and not allowing (as a merchant would) arbitrary payments, which opens up certain problems around snooping attackers (more on this below). So payjoin just means "A pays B but B actively participates and passes across utxos as extra inputs".

I'll defer a more features-focused and non-tech friendly description of what this means to a later blogpost.

Here is just the step by step protocol interactions, both at the user level and under-the-hood.

Alice will be Sender, Bob will be Receiver. Note that the Sender is a Taker class in JM code, and the Receiver a Maker class. This is because they inherit the main functional difference that one waits to complete the tx, and the other initiates.

Bob starts by running

python receive-payjoin.py -m mixdepth <walletname> <amount-in-sats>.

After normal Joinmarket start up (wallet sync, connect to message channels, also: Bob's bot puts a fake offer into the joinmarket pit; he effectively runs a spoofed yieldgenerator, but doesn't respond (for now) to requests in private-message)), Bob gets a printout like:

2018-12-27 17:57:17+0100 [-]  Your receiving address is:  2NCiuh9iJ3s8vEQGUWAfeZHTwUVqE5gEdB5
2018-12-27 17:57:17+0100 [-]  You will receive amount:  50000000  satoshis.
2018-12-27 17:57:17+0100 [-]  The sender also needs to know your ephemeral nickname:  J57Pwz4XHyyGu9QB
2018-12-27 17:57:17+0100 [-]  This information has been stored in a file payjoin.txt; send it  to your counterparty when you are ready.

This information is passed out of band to Alice (i.e. address, amount, nick). Bob's bot now waits as long as needed until Alice turns up and completes the protocol.

Alice runs as follows:

python sendpayment.py -m 1  <alicewalletname> 50000000 2NCiuh9iJ3s8vEQGUWAfeZHTwUVqE5gEdB5 -T J57Pwz4XHyyGu9QB

... using, of course, the data passed out of band. What happens next will usually not require further intervention, and after some seconds Alice will broadcast a transaction paying net 0.5 btc to Bob, in which at least one of his utxos is included.

  • Protocol

Alice sends in plaintext !pubkey <Alice-pubkey>. Bob receives and in plaintext also sends !pubkey <Bob-pubkey> back. These are of course ephemeral ECDH pubkeys used just for this session of communication.

Both sides now set up the end-to-end encryption (the existing codebase handles this automatically; both sides have crypto-box libnacl objects; and !tx messages are enforced to only be transferred under encryption).

Notice that any Alice-attacker can do this step, but it's harmless because the next step, which is encrypted, authenticates before Bob has sent any identifying info

Now Alice constructs a kind of dummy-tx: It takes form:

Inputs: <Alice utxos selected to pay .5> Outputs: <2NCiuh9iJ3s8vEQGUWAfeZHTwUVqE5gEdB5: 50000000>, <Alice-change: remainder>

Alice doesn't bother to calculate a fee here (so it's zero); this was not really a transaction, just a proposal of inputs and change. We send it as a transaction, because, as noted in the code comments:

however for the purposes of code reuse and signalling
that we are the right counterparty, sending a transaction,
containing the outputs, works fine.

The meaning of "signalling" here should be fairly obvious: by passing a transaction with the correct destination and amount, we ~ prove that we are the intended sender. Note that this line of reasoning is a bit too weak for the general merchant case, because anonymous senders operating over automated protocols may use this to make repeated spend request and collect your utxos, then not pay, damaging your privacy. The previously linked blog post I believe covers this issue in some detail (it was the subject of considerable debate in the London Coinjoin meetup).

The intention here is that for an ad-hoc payment this is not really a problem. However, TODO: I will alter the code so that Alice actually sends a signed version, she loses nothing this way and Bob can broadcast it if something goes wrong Alice-side.

So after Bob receives this !tx encrypted transaction message, he decrypts, and deserializes the transaction, checks that it conforms in destination, amount, and bitcoin balance, checks that the inputs are valid, then appends his own inputs. He selects coins to fulfil the same amount as the destination (this is a heuristic that could easily change if someone has a better idea; use twice the amount? half? etc.). If he fails to select that much he tries half as much, and repeats the halving three times before giving up, i.e. at the very least he adds 1/8 the amount of coins as he is receiving. Then he of course alters the output amount at the destination address to account for the coins that he's added. Finally, he also alters the change output amount proposed by Alice to account for the bitcoin network fee that he calculates. If it's dusty, he drops it. Finally if all checks pass he signs each input he himself has added (usually 1), and reserializes the now partially signed transaction. He then sends another !tx message in return to Alice, containing this partially signed transaction.

Alice receives, decrypts and deserializes, and does the expected checks: Checks the inputs that are signed, verify correctly. Checks that her utxos are included. Checks that the destination receives net the originally intended amount. Calculates the bitcoin fee and checks that it's between 0.3 and 3x the fee that her wallet estimates for current network conditions. If not, the user is prompted to decide. If all checks pass, signs all her inputs, and broadcasts the payment.

In short: Alice is a P2EPTaker and runs sendpayment. Bob is a P2EPMaker and runs receive-payjoin. They both use standard Joinmarket wallets. Alice connects and spoofs Taker behaviour, Bob connects (first) and spoofs Maker behaviour. Bob sends destn, amt and nick out of band. Alice starts with !pubkey, Bob responds the same, both sides set up encryption, Alice calculates and sends !tx encrypted proposal, Bob adds his input(s), signs and sends back !tx, Alice checks and if OK co-signs and broadcasts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment