24 changed files with 1754 additions and 76 deletions
@ -0,0 +1,384 @@ |
|||||||
|
### PayJoin (aka P2EP) user guide. |
||||||
|
|
||||||
|
(You've installed using the `install.sh` as per instructions in the README before |
||||||
|
reading this). |
||||||
|
|
||||||
|
This document does **not** discuss why PayJoin is interesting or the general concept. |
||||||
|
For that, see [this](TODO-BLOGPOST) post. |
||||||
|
|
||||||
|
Some instructions here will be redundant with the introductory [usage guide](USAGE.md); |
||||||
|
this guide is aimed at users who have not/ will not use Joinmarket for ordinary coinjoins. |
||||||
|
So just skip redundant info if you already know it. |
||||||
|
|
||||||
|
### Preparatory step: configuring for Bitcoin Core. |
||||||
|
|
||||||
|
Joinmarket currently requires a Bitcoin Core full node, although it can be pruned. |
||||||
|
|
||||||
|
First thing to do: in `scripts/`, run: |
||||||
|
|
||||||
|
python wallet-tool.py generate |
||||||
|
|
||||||
|
This *should* quit with an error, because the rpc is not configured. Open the newly created file `joinmarket.cfg`, |
||||||
|
and edit: |
||||||
|
|
||||||
|
[BLOCKCHAIN] |
||||||
|
rpc_user = yourusername-as-in-bitcoin.conf |
||||||
|
rpc_password = yourpassword-as-in-bitcoin.conf |
||||||
|
rpc_host = localhost #default usually correct |
||||||
|
rpc_port = 8332 # default for mainnet |
||||||
|
|
||||||
|
Note, you can also use a cookie file by setting, in this section, a variable `rpc_cookie_file` to the location of the file, |
||||||
|
as an alternative to using user/password. |
||||||
|
|
||||||
|
If you use Bitcoin Core's multiwallet feature, you can edit the value of `rpc_wallet_file` to your chosen wallet file. |
||||||
|
|
||||||
|
Then retry the same `generate` command; it should now not error - continue the generate process as per next step. |
||||||
|
|
||||||
|
If you still get rpc connection errors, make sure you can connect to your Core node using the command line first. |
||||||
|
|
||||||
|
### Make and fund the wallet |
||||||
|
|
||||||
|
Continue/complete the wallet generation with the above (`generate`) method. |
||||||
|
|
||||||
|
(Before you finish: want a bech32 wallet? you probably don't, |
||||||
|
but read [this](#what-if-i-wanted-bech32-native-segwit-addresses) in case.) |
||||||
|
|
||||||
|
The wallet you create is BIP49 by default, using BIP39 12 word seed, |
||||||
|
mnemonic extension optional (simplest to leave it out if you're not sure). |
||||||
|
|
||||||
|
Once the `generate` method run has completed, successfully, you need to look at the wallet contents. Use |
||||||
|
the `display` method which is the default: |
||||||
|
|
||||||
|
``` |
||||||
|
python wallet-tool.py wallet-name-you-chose.dat [display] |
||||||
|
``` |
||||||
|
|
||||||
|
("display" is optional because default; use `python wallet-tool.py -h` to see all possible methods). |
||||||
|
|
||||||
|
Below is [an example](#sample-testnet-wallet-display-output) of what the wallet looks like (although |
||||||
|
yours will be mainnet, so the addresses will start with '3' not '2'). |
||||||
|
|
||||||
|
Joinmarket by default uses *5* accounts, not only 1 as some other wallets (e.g. Electrum), this is to help |
||||||
|
with coin isolation. Try to move coins from one account to another *only* via coinjoins; or, you can just |
||||||
|
use one or two accounts (called "mixdepths" in Joinmarket parlance) as if they were just one, understanding |
||||||
|
that if you don't bother to do anything special, the coins in those two mixdepths are linked. |
||||||
|
|
||||||
|
Fund the wallet by choosing one or more addresses in the *external* section of the first account (called |
||||||
|
"Mixdepth 0" here). When you fund, fund to the external addresses. The internals will be used for change. |
||||||
|
|
||||||
|
(The new standard (BIP49) *should* be compatible with TREZOR, Ledger, Electrum, Samourai and some others, |
||||||
|
including the 12 word seed, although consider privacy concerns when sending addresses to remote servers!). |
||||||
|
|
||||||
|
### Doing a PayJoin payment. |
||||||
|
|
||||||
|
(At the end of this file are full terminal outputs from a regtest run of the process, |
||||||
|
you can read it after this to see that it makes sense; there's also a video |
||||||
|
[here](https://joinmarket.me/blog/blog/payjoin-basic-demo) of the process running live with mainnet coins). |
||||||
|
|
||||||
|
* Receiver needs to start: run (still in scripts/ directory): |
||||||
|
|
||||||
|
``` |
||||||
|
python receive-payjoin.py -m 1 receiver-wallet-name.jmdat amount-in-satoshis |
||||||
|
``` |
||||||
|
|
||||||
|
Note : `-m 1` is choosing the *mixdepth* (see above) to *spend* coins from: in a payjoin, |
||||||
|
the receiver also spends some coins, he just gets that amount back, as well as his payment. |
||||||
|
|
||||||
|
If you funded into mixdepth 0 at the start, and you only have coins there, you must choose 0 |
||||||
|
here (which is the default). How much do you need? The code is fairly lenient and it doesn't |
||||||
|
matter too much. But it needs to be more than zero! |
||||||
|
|
||||||
|
The receiver will be prompted to read/note the receiving address, amount and "ephemeral nick", and then |
||||||
|
the script will just wait (indefinitely). It'll look something like this: |
||||||
|
|
||||||
|
``` |
||||||
|
2019-01-16 16:11:40,018 [INFO] Your receiving address is: 2NA65YN6eXf3LiciBb1dEdS6ovaZ8HVBcHS |
||||||
|
2019-01-16 16:11:40,018 [INFO] You will receive amount: 27000000 satoshis. |
||||||
|
2019-01-16 16:11:40,018 [INFO] The sender also needs to know your ephemeral nickname: J5AFezpsuV95CBCH |
||||||
|
2019-01-16 16:11:40,018 [INFO] This information has been stored in a file payjoin.txt; send it to your counterparty when you are ready. |
||||||
|
``` |
||||||
|
|
||||||
|
The "ephemeral nick" starts with J, version (5 currently), then a short base58 string. It's how the sender will find you in the |
||||||
|
"joinmarket trading pit" (multiple IRC servers are used for this currently). |
||||||
|
|
||||||
|
* Receiver sends data to sender (amount, address, ephemeral nick). |
||||||
|
|
||||||
|
This data is stored in the file payjoin.txt but not currently using any encoding (that's a TODO). |
||||||
|
|
||||||
|
* Sender starts up the sendpayment script: |
||||||
|
|
||||||
|
``` |
||||||
|
python sendpayment.py -m 1 receiver-wallet.jmdat 27000000 2NA65YN6eXf3LiciBb1dEdS6ovaZ8HVBcHS -T J5AFezpsuV95CBCH |
||||||
|
``` |
||||||
|
|
||||||
|
Notice that the user has specified the three pieces of data given; using the `-T` flags this as a PayJoin; if you don't do this you will be |
||||||
|
doing a Joinmarket coinjoin accidentally! (which wouldn't be the end of the world, the receiver would still get the money!). |
||||||
|
|
||||||
|
As before -m 1 tells the wallet which mixdepth to source coins to spend from; it obviously needs to have at least the given amount |
||||||
|
(in this case, 27 million satoshis or 0.27 btc). |
||||||
|
|
||||||
|
* The two sides communicate |
||||||
|
|
||||||
|
This takes generally only a couple of seconds, not including a relatively slow startup (30s-60s or so depending on configuration), |
||||||
|
during which time the sender acts similarly to any other Joinmarket participant (and so does the receiver, likewise), to have |
||||||
|
a slightly better "blend in with the crowd" effect. |
||||||
|
|
||||||
|
Once the transaction is broadcast, you'll see a message to that effect and the script will shutdown. You can check the txid |
||||||
|
on your favourite block explorer. |
||||||
|
|
||||||
|
* What if something goes wrong and the payment fails to go through? |
||||||
|
|
||||||
|
First, if the sender succeeds in doing the first step, the receiver will have a non-coinjoin ordinary payment transaction |
||||||
|
to fall back on. It'll look like this: |
||||||
|
|
||||||
|
``` |
||||||
|
2019-01-16 16:34:55,168 [INFO] Network transaction fee is: 1672 satoshis. |
||||||
|
2019-01-16 16:34:55,175 [INFO] We'll use this serialized transaction to broadcast if your counterparty fails to broadcast the payjoin version: |
||||||
|
2019-01-16 16:34:55,177 [INFO] 0200000000010152da645b5a2ec3a166ad8a933b1442fa38fc119faf7659d72033ba863fdd8d470000000017160014297b55001daa905a3552137ef19755cf4eae7babfeffffff02b8be4f0a0000000017a914e3cb2bf72ccd4412035f9668e07e17b6c9ebd58d87c0fc9b010000000017a914b8bf57f3bae00d23f9a60c9a6e4d4c7182ec87cc8702483045022100e3605548f5e07ebd14a0700dc3e54e7c8ae90ce0057a3fdc80b5fd37636b44a002202fd7942104fb344b80b8fd91faf0394a0f274f7a68fa462084b8ecf2fa245a65012102f823d62891d8bc8544d4369bb98c6fb8235a372d2c36196d40c448690b42754f42030000 |
||||||
|
2019-01-16 16:34:55,186 [INFO] The proposed tx does not trigger UIH2, which means it is indistinguishable from a normal payment. This is the ideal case. Continuing.. |
||||||
|
.... |
||||||
|
``` |
||||||
|
|
||||||
|
If the final step fails and we don't get a PayJoin, the receiver can just use his favorite broadcast method (e.g. |
||||||
|
`sendrawtransaction` in Bitcoin Core) to send the signed transaction above (02000...). |
||||||
|
|
||||||
|
* Privacy and security controls |
||||||
|
|
||||||
|
Just like Joinmarket, do note, that here the private information is communicated with **end-to-end encryption** - it's not like |
||||||
|
the people/operators on the IRC servers are going to learn any information about your transaction. |
||||||
|
|
||||||
|
Second, it's very recommended to use Tor connections to the messaging servers via their hidden services. See the |
||||||
|
`[MESSAGING:serverN]` sections in the `joinmarket.cfg`. This keeps anyone from seeing your IP address origin. |
||||||
|
|
||||||
|
This altogether means it's possible to have anonymity from your sender/receiver counterparty, if that's an issue. |
||||||
|
You *do* still have to send them the payment information out of band, though. We could perhaps fold this in, although |
||||||
|
it seems a bit tricky to do so without introducing a security issue. |
||||||
|
|
||||||
|
An additional minor feature is that the receiver "fakes" being a Joinmarket maker, so his bot looks the same as the other |
||||||
|
ones in the Joinmarket trading pit (at least, to a crude extent). And the sender fakes being a Joinmarket taker, too. |
||||||
|
|
||||||
|
Also we try to make the PayJoin look as much as possible as an ordinary payment. For example: |
||||||
|
- we make the transaction version, locktime and sequence numbers look similar to those created by Core, Electrum. |
||||||
|
- we try to avoid "UIH2", meaning we avoid a situation where one input is larger than any output, since that would |
||||||
|
mean no other inputs are needed; wallet coin selection algorithms *usually* don't do that. |
||||||
|
|
||||||
|
Security - since the receiver passively waits, what happens if a bad actor tries to connect to him? Nothing; an attacker |
||||||
|
would fail to even start the process without knowing the payment amount and address, which the receiver is not broadcasting |
||||||
|
around everywhere (especially not the amount and ephemeral nickname), and even if they knew that, the worst |
||||||
|
they can do is learn at least 1 utxo of the receiver. The receiver won't pay attention to non-PayJoin messages, either. |
||||||
|
|
||||||
|
#### What if I wanted bech32 native segwit addresses? |
||||||
|
|
||||||
|
You can do this, but bear in mind: PayJoin only gives its full effect if you and your receiver are using |
||||||
|
the same kind of addresses; so do this only if you and your receiver(s)/sender(s) agree on it. |
||||||
|
|
||||||
|
Also note: you *cannot* do Joinmarket coinjoins if you choose a bech32 wallet (this may change in future). |
||||||
|
|
||||||
|
In the configuration file `joinmarket.cfg` (which was created in the preparatory step above), go to the |
||||||
|
POLICY section and set: |
||||||
|
|
||||||
|
``` |
||||||
|
[POLICY] |
||||||
|
native = true |
||||||
|
``` |
||||||
|
|
||||||
|
Note that this must be done *before* generating the wallet, as |
||||||
|
the wallet internally, of course, stores which type of addresses it manages, and it can only be of two |
||||||
|
types currently (ignoring legacy upgrades): bech32 or p2sh-segwit (starting with '3'), the latter being |
||||||
|
the default (and the one used in Joinmarket itself). |
||||||
|
|
||||||
|
Note that the bech32 style wallet is written to conform to [BIP84](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki), |
||||||
|
analogous to the BIP49 case for p2sh. |
||||||
|
|
||||||
|
#### Sample testnet wallet display output |
||||||
|
|
||||||
|
``` |
||||||
|
JM wallet |
||||||
|
mixdepth 0 tpubDC4qk8DsyiFYY85uktoKaiR1srWLSNRxZ2A4MXYg5LHy9XHKSTcF2tNtFpGz4LzXPcDH1kNkiww7MwipNqNSps6HSyjPTqTB18J7C4jrFMi |
||||||
|
external addresses m/49'/1'/0'/0 tpubDFFCJfi4y6xPKquA6H6aP5EBZPupb6A9tJz8sErW8GN6D7D3s9MABPt1BczpQ3n8rBv7VLSVpu3ddvb7xEKMfNX2sMZ7XxiBD4J6kpfF7Ag |
||||||
|
m/49'/1'/0'/0/0 2NBT6npWKxBEG8fkDjSFLDZJ7fNda4kYnAx 2.00000000 deposit |
||||||
|
m/49'/1'/0'/0/1 2Mt7dBFikYwCQTDU129VTF9ahKWxeJjEUuF 0.00000000 new |
||||||
|
m/49'/1'/0'/0/2 2N94bte8xWojSaX6yq2th3R3mUhvKzZqDJ9 0.00000000 new |
||||||
|
m/49'/1'/0'/0/3 2MvSBTkCKwPHLdNPRXbovPXUM8oAfjUFPYc 0.00000000 new |
||||||
|
m/49'/1'/0'/0/4 2MzVn3Nc3RRyN7shiVajA225xCTcaGn1PRw 0.00000000 new |
||||||
|
m/49'/1'/0'/0/5 2MxJwv2dmkMupDuBLsEMa4HgG6GAieWHiXr 0.00000000 new |
||||||
|
m/49'/1'/0'/0/6 2MwhdkeAcnCkam1LdVBQJ7un8syyoYB1HVH 0.00000000 new |
||||||
|
Balance: 2.00000000 |
||||||
|
internal addresses m/49'/1'/0'/1 |
||||||
|
Balance: 0.00000000 |
||||||
|
Balance for mixdepth 0: 2.00000000 |
||||||
|
mixdepth 1 tpubDC4qk8DsyiFYba4t8cSpadjoLYUdPwV5dAtBpzpPzgaDKPfSP42xNJq48QUKEVGHQRfFej6DCUjQqCKD8TtcqN932f27jmyjXaxVMpksos4 |
||||||
|
external addresses m/49'/1'/1'/0 tpubDEnijtftQiJpVgezdRNyKVVWGr9xKV9RgPQWHYDHpQ5utdHF7Sqh7xMUyNHcpdeKcKdQ753hFbyccRZNEHTUkHLnDaVqoXRo9XkATPtHhCp |
||||||
|
m/49'/1'/1'/0/0 2NEsN45waxdkqjP5EnsP3K8YjMeZJh2RLx6 2.00000000 deposit |
||||||
|
m/49'/1'/1'/0/1 2Mzhewfg6fr5jR122txdTShmLi7rH9ZscTm 2.00000000 deposit |
||||||
|
m/49'/1'/1'/0/2 2N1vn44cv5m6PhRJMbZ1mR8dAJgHybnsT5u 2.00000000 deposit |
||||||
|
m/49'/1'/1'/0/3 2NFwBsHkQ8mxCJWRJhkxAY28Tj8YuUJio4t 0.00000000 new |
||||||
|
m/49'/1'/1'/0/4 2NA1YAa2VqHMSH9b2GYGBA8waUMxZTZwW5Z 0.00000000 new |
||||||
|
m/49'/1'/1'/0/5 2MvLLp4cnP8ZVuWWDZzRCLFM2YcTfo9ALec 0.00000000 new |
||||||
|
m/49'/1'/1'/0/6 2Mux3ZUHGmaMBiMsPDbQS56gRGGN26jMGzd 0.00000000 new |
||||||
|
m/49'/1'/1'/0/7 2N3MWFiSHyRY3QLZgb63Vfcp4BGSFs6Y3bV 0.00000000 new |
||||||
|
m/49'/1'/1'/0/8 2N3z5zKJPTfPc41esRRvMvjdKaSv6D6jqvY 0.00000000 new |
||||||
|
Balance: 6.00000000 |
||||||
|
internal addresses m/49'/1'/1'/1 |
||||||
|
Balance: 0.00000000 |
||||||
|
Balance for mixdepth 1: 6.00000000 |
||||||
|
mixdepth 2 tpubDC4qk8DsyiFYdQBNNzo3vHq63Gag4eUJT29UaTVNHM89hJk6CshZ9WGemQNDh2LGDXCud8anAQ4UR7n7tSWiJtviR8WJuTB78ZbEHpFNcLH |
||||||
|
external addresses m/49'/1'/2'/0 tpubDEY4sVvs1TX82DftUUB51Agg4Ln7BksoRGESNWtrWTDntV2fCs5wNrqiPENcXBAEHtnaY9ZaK48PRFEw1GLhcTxdDNHUyuqDd2YyNYKoVAo |
||||||
|
m/49'/1'/2'/0/0 2NBSnSB3nVN4TA7EcNkhcRsmrdyXSQepDFE 0.00000000 new |
||||||
|
m/49'/1'/2'/0/1 2NFtrtpdvmCRXMy1fV8w1eLWpF8MC3nre7n 0.00000000 new |
||||||
|
m/49'/1'/2'/0/2 2NFHJvGLWU7KuNkbo8rzPwfGtCS5RtNDw7c 0.00000000 new |
||||||
|
m/49'/1'/2'/0/3 2NGJcmeScFnTSzZzSRNHiLL8zjYeA9ngx5A 0.00000000 new |
||||||
|
m/49'/1'/2'/0/4 2NBUAxKrNQtp49xYYHh9f6YHfo2FvYBP1NL 0.00000000 new |
||||||
|
m/49'/1'/2'/0/5 2MyQfzKyPTfYT4vTe2d3fyGvQMoGX6GAhcr 0.00000000 new |
||||||
|
Balance: 0.00000000 |
||||||
|
internal addresses m/49'/1'/2'/1 |
||||||
|
Balance: 0.00000000 |
||||||
|
Balance for mixdepth 2: 0.00000000 |
||||||
|
mixdepth 3 tpubDC4qk8DsyiFYgXoNb3UmiaG2veSTdrLCxEBUsTEQnDgQLCaC8Yg1z1Bcwn4ZQivNhgBxHEWH7j8hbRx7rab2kYLy4r4PXxor4Ho3A5AJvVH |
||||||
|
external addresses m/49'/1'/3'/0 tpubDEnwog2atqaSLe9xVTTdqxY5ynysqeQPsXuaKrZ6HaYCJcFPY7LmhepmwFTJYTkqf1w5jgLQmoZvREyk9qiq4P2A2fSGTyj62WUE4VjXK76 |
||||||
|
m/49'/1'/3'/0/0 2MtckYQLD6bJZiPXffW7m5rryMao7u4ktng 0.00000000 new |
||||||
|
m/49'/1'/3'/0/1 2N9KrH5Bi35ZH3DVUwD67eh8MtQD1LLhgCa 0.00000000 new |
||||||
|
m/49'/1'/3'/0/2 2N6zLabMeXTXfyzQttr6PBV2JdbZPrVd9i8 0.00000000 new |
||||||
|
m/49'/1'/3'/0/3 2MzYTWMjXcv4NBce5PKcDAceT7gMuYrGovc 0.00000000 new |
||||||
|
m/49'/1'/3'/0/4 2N6tYykyCZLtvP1RJZvgZ9a7c6xbQaqpE66 0.00000000 new |
||||||
|
m/49'/1'/3'/0/5 2N2o3eJ9h2BC5q3TzPVqhn9gmgWBjaL67Hu 0.00000000 new |
||||||
|
Balance: 0.00000000 |
||||||
|
internal addresses m/49'/1'/3'/1 |
||||||
|
Balance: 0.00000000 |
||||||
|
Balance for mixdepth 3: 0.00000000 |
||||||
|
mixdepth 4 tpubDC4qk8DsyiFYj7GF4LV97c9f7Yff1mrwnxkpi5twGSLmesmPyM4xXBWsxMw9ZLFycVVC4TeeX1ESjNP4rVVrJEDVCm7C3UMvZH9vs6srsAi |
||||||
|
external addresses m/49'/1'/4'/0 tpubDFA3XKgf2ZiusZHr4we5utkrQ9toN5s7QGKndNMrdQFQfjQU6yiiMT65tmXFCPduSc7muLFegAi36pv4LCdRnhpRYp2QUpm1izyrboWSjzV |
||||||
|
m/49'/1'/4'/0/0 2N18HGRJyaaUwLQFTyfjqZoXGCV8Yv5rbwD 0.00000000 new |
||||||
|
m/49'/1'/4'/0/1 2Mt94Y4iguYLkBAhsXT5a1P8VMTQ4kxdZJD 0.00000000 new |
||||||
|
m/49'/1'/4'/0/2 2N5vYyG1gx8ht2JCKkuVqow3Qn51cHrwaxh 0.00000000 new |
||||||
|
m/49'/1'/4'/0/3 2N44ZtKYu21p1qN4DBoTCYZL2sms6YBeWVW 0.00000000 new |
||||||
|
m/49'/1'/4'/0/4 2NF9p2b2PfHvXiPmPP5JPq4CRxKn47LPZrW 0.00000000 new |
||||||
|
m/49'/1'/4'/0/5 2MwgfAvbPc4ASD84WdBYo2FM5bXBVG1rRG9 0.00000000 new |
||||||
|
Balance: 0.00000000 |
||||||
|
internal addresses m/49'/1'/4'/1 |
||||||
|
Balance: 0.00000000 |
||||||
|
Balance for mixdepth 4: 0.00000000 |
||||||
|
Total balance: 8.00000000 |
||||||
|
``` |
||||||
|
|
||||||
|
#### Full sender-side log of a regtest run of PayJoin |
||||||
|
|
||||||
|
(Note that the "Received offers from joinmarket pit" message is a privacy feature, we don't actually respond to any offers). |
||||||
|
|
||||||
|
``` |
||||||
|
(jmvenv) me@here:~/jm/scripts$ python sendpayment.py -m1 b80d142a466dbf56f518a3a8c017ab85 27000000 2NA65YN6eXf3LiciBb1dEdS6ovaZ8HVBcHS -T J5AFezpsuV95CBCH |
||||||
|
2019-01-16 16:34:44,877 [WARNING] Cannot listen on port 27183, trying next port |
||||||
|
2019-01-16 16:34:44,877 [WARNING] Cannot listen on port 27184, trying next port |
||||||
|
2019-01-16 16:34:44,877 [WARNING] Cannot listen on port 27185, trying next port |
||||||
|
2019-01-16 16:34:44,878 [WARNING] Cannot listen on port 27186, trying next port |
||||||
|
2019-01-16 16:34:44,878 [INFO] Listening on port 27187 |
||||||
|
2019-01-16 16:34:47,495 [INFO] Could not connect to *ALL* servers yet, waiting up to 60 more seconds. |
||||||
|
2019-01-16 16:34:47,496 [INFO] All IRC servers connected, starting execution. |
||||||
|
2019-01-16 16:34:47,501 [INFO] JM daemon setup complete |
||||||
|
2019-01-16 16:34:52,508 [INFO] INFO:Received offers from joinmarket pit |
||||||
|
2019-01-16 16:34:52,531 [INFO] total estimated amount spent = 27020000 |
||||||
|
2019-01-16 16:34:52,594 [INFO] Makers responded with: ["J5AFezpsuV95CBCH"] |
||||||
|
2019-01-16 16:34:52,600 [INFO] Obtained proposed payjoin tx |
||||||
|
{'ins': [{'outpoint': {'hash': '478ddd3f86ba3320d75976af9f11fc38fa42143b938aad66a1c32e5a5b64da52', |
||||||
|
'index': 0}, |
||||||
|
'script': '', |
||||||
|
'sequence': 4294967294}], |
||||||
|
'locktime': 834, |
||||||
|
'outs': [{'script': 'a914e3cb2bf72ccd4412035f9668e07e17b6c9ebd58d87', |
||||||
|
'value': 172998328}, |
||||||
|
{'script': 'a914b8bf57f3bae00d23f9a60c9a6e4d4c7182ec87cc87', |
||||||
|
'value': 27000000}], |
||||||
|
'version': 2} |
||||||
|
2019-01-16 16:34:52,602 [INFO] INFO:Built tx proposal, sending to receiver. |
||||||
|
2019-01-16 16:34:56,622 [INFO] Obtained tx from receiver: |
||||||
|
{'ins': [{'outpoint': {'hash': '478ddd3f86ba3320d75976af9f11fc38fa42143b938aad66a1c32e5a5b64da52', |
||||||
|
'index': 0}, |
||||||
|
'script': '', |
||||||
|
'sequence': 4294967294, |
||||||
|
'txinwitness': []}, |
||||||
|
{'outpoint': {'hash': '5e19edacc10298e7761be7231db5fa44ad73903c3690c4b71b443355267d346a', |
||||||
|
'index': 0}, |
||||||
|
'script': '160014a5b07a64bff72b442c71761599e0b627c687661f', |
||||||
|
'sequence': 4294967294, |
||||||
|
'txinwitness': ['3044022077a811939910a935934f68ac8a70439bab665ce9ad4cd02e42289c3046d606e1022054ef241980733caa7c7c5b6bb1babf96caba83a9fb0a8e091d8358b1cb35fcdb01', |
||||||
|
'02c7ceecf0783ecc7530e018397db434af63eda63765c79816f023699a22663926']}], |
||||||
|
'locktime': 834, |
||||||
|
'outs': [{'script': 'a914b8bf57f3bae00d23f9a60c9a6e4d4c7182ec87cc87', |
||||||
|
'value': 227000000}, |
||||||
|
{'script': 'a914e3cb2bf72ccd4412035f9668e07e17b6c9ebd58d87', |
||||||
|
'value': 172997415}], |
||||||
|
'version': 2} |
||||||
|
2019-01-16 16:34:56,630 [INFO] INFO:Network transaction fee is: 2585 satoshis. |
||||||
|
2019-01-16 16:34:56,641 [INFO] txid = 1031780b31bd3b1cfdec44296fdb82e467284e93f871eb48d7d3e72df059f0ae |
||||||
|
2019-01-16 16:34:56,661 [INFO] Transaction broadcast OK. |
||||||
|
2019-01-16 16:35:01,651 [INFO] Transaction seen on network, shutting down. |
||||||
|
2019-01-16 16:35:01,652 [INFO] Txid was: 1031780b31bd3b1cfdec44296fdb82e467284e93f871eb48d7d3e72df059f0ae |
||||||
|
``` |
||||||
|
|
||||||
|
#### Full receiver-side log of a regtest run of PayJoin |
||||||
|
|
||||||
|
``` |
||||||
|
(jmvenv) me@here:~/jm/scripts$ python receive-payjoin.py -m1 84735f364c2cf4c8ddaa614315aeae14 27000000 |
||||||
|
2019-01-16 16:11:39,952 [INFO] offerlist=[{'maxsize': 112412265, 'ordertype': 'swreloffer', 'cjfee': '0.00035792', 'oid': 0, 'minsize': 1095839, 'txfee': 493}] |
||||||
|
2019-01-16 16:11:39,952 [INFO] starting receive-payjoin |
||||||
|
2019-01-16 16:11:40,011 [WARNING] Cannot listen on port 27183, trying next port |
||||||
|
2019-01-16 16:11:40,011 [WARNING] Cannot listen on port 27184, trying next port |
||||||
|
2019-01-16 16:11:40,011 [WARNING] Cannot listen on port 27185, trying next port |
||||||
|
2019-01-16 16:11:40,012 [INFO] Listening on port 27186 |
||||||
|
2019-01-16 16:11:40,018 [INFO] Your receiving address is: 2NA65YN6eXf3LiciBb1dEdS6ovaZ8HVBcHS |
||||||
|
2019-01-16 16:11:40,018 [INFO] You will receive amount: 27000000 satoshis. |
||||||
|
2019-01-16 16:11:40,018 [INFO] The sender also needs to know your ephemeral nickname: J5AFezpsuV95CBCH |
||||||
|
2019-01-16 16:11:40,018 [INFO] This information has been stored in a file payjoin.txt; send it to your counterparty when you are ready. |
||||||
|
2019-01-16 16:35:00+0100 [-] Enter 'y' to wait for the payment:y |
||||||
|
2019-01-16 16:34:32,449 [INFO] Could not connect to *ALL* servers yet, waiting up to 60 more seconds. |
||||||
|
2019-01-16 16:34:32,450 [INFO] All IRC servers connected, starting execution. |
||||||
|
2019-01-16 16:34:32,453 [INFO] JM daemon setup complete |
||||||
|
|
||||||
|
(note time gap here; just waiting) |
||||||
|
|
||||||
|
2019-01-16 16:34:55,163 [INFO] obtained tx proposal from sender: |
||||||
|
{'ins': [{'outpoint': {'hash': '478ddd3f86ba3320d75976af9f11fc38fa42143b938aad66a1c32e5a5b64da52', |
||||||
|
'index': 0}, |
||||||
|
'script': '160014297b55001daa905a3552137ef19755cf4eae7bab', |
||||||
|
'sequence': 4294967294, |
||||||
|
'txinwitness': ['3045022100e3605548f5e07ebd14a0700dc3e54e7c8ae90ce0057a3fdc80b5fd37636b44a002202fd7942104fb344b80b8fd91faf0394a0f274f7a68fa462084b8ecf2fa245a6501', |
||||||
|
'02f823d62891d8bc8544d4369bb98c6fb8235a372d2c36196d40c448690b42754f']}], |
||||||
|
'locktime': 834, |
||||||
|
'outs': [{'script': 'a914e3cb2bf72ccd4412035f9668e07e17b6c9ebd58d87', |
||||||
|
'value': 172998328}, |
||||||
|
{'script': 'a914b8bf57f3bae00d23f9a60c9a6e4d4c7182ec87cc87', |
||||||
|
'value': 27000000}], |
||||||
|
'version': 2} |
||||||
|
2019-01-16 16:34:55,165 [WARNING] Connection had broken pipe, attempting reconnect. |
||||||
|
2019-01-16 16:34:55,168 [INFO] Network transaction fee is: 1672 satoshis. |
||||||
|
2019-01-16 16:34:55,175 [INFO] We'll use this serialized transaction to broadcast if your counterparty fails to broadcast the payjoin version: |
||||||
|
2019-01-16 16:34:55,177 [INFO] 0200000000010152da645b5a2ec3a166ad8a933b1442fa38fc119faf7659d72033ba863fdd8d470000000017160014297b55001daa905a3552137ef19755cf4eae7babfeffffff02b8be4f0a0000000017a914e3cb2bf72ccd4412035f9668e07e17b6c9ebd58d87c0fc9b010000000017a914b8bf57f3bae00d23f9a60c9a6e4d4c7182ec87cc8702483045022100e3605548f5e07ebd14a0700dc3e54e7c8ae90ce0057a3fdc80b5fd37636b44a002202fd7942104fb344b80b8fd91faf0394a0f274f7a68fa462084b8ecf2fa245a65012102f823d62891d8bc8544d4369bb98c6fb8235a372d2c36196d40c448690b42754f42030000 |
||||||
|
2019-01-16 16:34:55,186 [INFO] The proposed tx does not trigger UIH2, which means it is indistinguishable from a normal payment. This is the ideal case. Continuing.. |
||||||
|
2019-01-16 16:34:55,187 [INFO] We selected inputs worth: 200000000 |
||||||
|
2019-01-16 16:34:55,195 [INFO] We estimated a fee of: 2585 |
||||||
|
2019-01-16 16:34:55,196 [INFO] We calculated a new change amount of: 172997415 |
||||||
|
2019-01-16 16:34:55,197 [INFO] We calculated a new destination amount of: 227000000 |
||||||
|
2019-01-16 16:35:00,232 [INFO] The transaction has been broadcast. |
||||||
|
2019-01-16 16:35:00,232 [INFO] Txid is: 1031780b31bd3b1cfdec44296fdb82e467284e93f871eb48d7d3e72df059f0ae |
||||||
|
2019-01-16 16:35:00,233 [INFO] Transaction in detail: {'ins': [{'outpoint': {'hash': '478ddd3f86ba3320d75976af9f11fc38fa42143b938aad66a1c32e5a5b64da52', |
||||||
|
'index': 0}, |
||||||
|
'script': '160014297b55001daa905a3552137ef19755cf4eae7bab', |
||||||
|
'sequence': 4294967294, |
||||||
|
'txinwitness': ['3045022100c1d579fb19f5b8710b407e3995657313d0b1c9fe1180b852834b2210ba19b5e30220464f034eef8d8975b4fe10db7edbf14fcfdc6d71ada3d2063d34fa5c04a38cb801', |
||||||
|
'02f823d62891d8bc8544d4369bb98c6fb8235a372d2c36196d40c448690b42754f']}, |
||||||
|
{'outpoint': {'hash': '5e19edacc10298e7761be7231db5fa44ad73903c3690c4b71b443355267d346a', |
||||||
|
'index': 0}, |
||||||
|
'script': '160014a5b07a64bff72b442c71761599e0b627c687661f', |
||||||
|
'sequence': 4294967294, |
||||||
|
'txinwitness': ['3044022077a811939910a935934f68ac8a70439bab665ce9ad4cd02e42289c3046d606e1022054ef241980733caa7c7c5b6bb1babf96caba83a9fb0a8e091d8358b1cb35fcdb01', |
||||||
|
'02c7ceecf0783ecc7530e018397db434af63eda63765c79816f023699a22663926']}], |
||||||
|
'locktime': 834, |
||||||
|
'outs': [{'script': 'a914b8bf57f3bae00d23f9a60c9a6e4d4c7182ec87cc87', |
||||||
|
'value': 227000000}, |
||||||
|
{'script': 'a914e3cb2bf72ccd4412035f9668e07e17b6c9ebd58d87', |
||||||
|
'value': 172997415}], |
||||||
|
'version': 2} |
||||||
|
2019-01-16 16:35:00,234 [INFO] shutting down. |
||||||
|
``` |
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,163 @@ |
|||||||
|
from __future__ import (absolute_import, division, |
||||||
|
print_function, unicode_literals) |
||||||
|
from builtins import * # noqa: F401 |
||||||
|
|
||||||
|
""" |
||||||
|
Test doing payjoin joins (with message channel layer mocked) |
||||||
|
""" |
||||||
|
|
||||||
|
import os |
||||||
|
import sys |
||||||
|
import pytest |
||||||
|
from twisted.internet import reactor |
||||||
|
from jmbase import get_log |
||||||
|
from jmclient import cryptoengine |
||||||
|
from jmclient import (load_program_config, jm_single, sync_wallet, |
||||||
|
P2EPMaker, P2EPTaker, |
||||||
|
LegacyWallet, SegwitLegacyWallet, SegwitWallet) |
||||||
|
from commontest import make_wallets |
||||||
|
from test_coinjoin import make_wallets_to_list, sync_wallets, create_orderbook |
||||||
|
|
||||||
|
testdir = os.path.dirname(os.path.realpath(__file__)) |
||||||
|
log = get_log() |
||||||
|
|
||||||
|
def create_taker(wallet, schedule, monkeypatch): |
||||||
|
taker = P2EPTaker("fakemaker", wallet, schedule, |
||||||
|
callbacks=(None, None, None)) |
||||||
|
return taker |
||||||
|
|
||||||
|
def dummy_user_check(message): |
||||||
|
# No user interaction in test; just print message |
||||||
|
# and assume acceptance. |
||||||
|
log.info(message) |
||||||
|
return True |
||||||
|
|
||||||
|
def getbals(wallet, mixdepth): |
||||||
|
""" Retrieves balances for a mixdepth and the 'next' |
||||||
|
""" |
||||||
|
bbm = wallet.get_balance_by_mixdepth() |
||||||
|
return (bbm[mixdepth], bbm[(mixdepth + 1) % (wallet.mixdepth + 1)]) |
||||||
|
|
||||||
|
def final_checks(wallets, amount, txfee, tsb, msb, source_mixdepth=0): |
||||||
|
"""We use this to check that the wallet contents are |
||||||
|
as we've expected according to the test case. |
||||||
|
amount is the payment amount going from taker to maker. |
||||||
|
txfee is the bitcoin network transaction fee, paid by the |
||||||
|
taker. |
||||||
|
tsb, msb are taker and maker starting balances, each a tuple |
||||||
|
of two entries, source and destination mixdepth respectively. |
||||||
|
""" |
||||||
|
jm_single().bc_interface.tickchain() |
||||||
|
for wallet in wallets: |
||||||
|
sync_wallet(wallet) |
||||||
|
takerbals = getbals(wallets[1], source_mixdepth) |
||||||
|
makerbals = getbals(wallets[0], source_mixdepth) |
||||||
|
# is the payment received? |
||||||
|
maker_newcoin_amt = makerbals[1] - msb[1] |
||||||
|
if not maker_newcoin_amt >= amount: |
||||||
|
print("Maker expected to receive at least: ", amount, |
||||||
|
" but got: ", maker_newcoin_amt) |
||||||
|
return False |
||||||
|
# assert that the maker received net exactly the right amount |
||||||
|
maker_spentcoin_amt = msb[0] - makerbals[0] |
||||||
|
if not maker_spentcoin_amt >= 0: |
||||||
|
# for now allow the non-cj fallback case |
||||||
|
print("maker's spent coin should have been positive, was: ", maker_spentcoin_amt) |
||||||
|
return False |
||||||
|
if not maker_newcoin_amt == amount + maker_spentcoin_amt: |
||||||
|
print("maker's new coins should have been: ", amount + maker_spentcoin_amt, |
||||||
|
" but was: ", maker_newcoin_amt) |
||||||
|
return False |
||||||
|
|
||||||
|
# Taker-side check |
||||||
|
# assert that the taker's total ending minus total starting |
||||||
|
# balance is the amount plus the txfee given. |
||||||
|
if not (sum(takerbals) - sum(tsb) + txfee + amount) == 0: |
||||||
|
print("Taker should have spent: ", txfee + amount, |
||||||
|
" but spent: ", sum(tsb) - sum(takerbals)) |
||||||
|
return False |
||||||
|
return True |
||||||
|
|
||||||
|
@pytest.mark.parametrize('wallet_cls, wallet_structures, mean_amt', |
||||||
|
[([LegacyWallet, LegacyWallet], |
||||||
|
[[4, 0, 0, 0, 0]] * 2, 1.0), |
||||||
|
([SegwitLegacyWallet, SegwitLegacyWallet], |
||||||
|
[[1, 3, 0, 0, 0]] * 2, 2.0), |
||||||
|
([SegwitWallet, SegwitWallet], |
||||||
|
[[1, 0, 0, 0, 0]] * 2, 4.0), |
||||||
|
([LegacyWallet, SegwitWallet], |
||||||
|
[[4, 0, 0, 0, 0]] * 2, 1.0), |
||||||
|
([SegwitLegacyWallet, SegwitWallet], |
||||||
|
[[1, 3, 0, 0, 0]] * 2, 2.0), |
||||||
|
([SegwitWallet, SegwitLegacyWallet], |
||||||
|
[[1, 0, 0, 0, 0]] * 2, 4.0), |
||||||
|
]) |
||||||
|
def test_simple_payjoin(monkeypatch, tmpdir, setup_cj, wallet_cls, |
||||||
|
wallet_structures, mean_amt): |
||||||
|
def raise_exit(i): |
||||||
|
raise Exception("sys.exit called") |
||||||
|
monkeypatch.setattr(sys, 'exit', raise_exit) |
||||||
|
wallets = [] |
||||||
|
wallets.append(make_wallets_to_list(make_wallets( |
||||||
|
1, wallet_structures=[wallet_structures[0]], |
||||||
|
mean_amt=mean_amt, wallet_cls=wallet_cls[0]))[0]) |
||||||
|
wallets.append(make_wallets_to_list(make_wallets( |
||||||
|
1, wallet_structures=[wallet_structures[1]], |
||||||
|
mean_amt=mean_amt, wallet_cls=wallet_cls[1]))[0]) |
||||||
|
jm_single().bc_interface.tickchain() |
||||||
|
sync_wallets(wallets) |
||||||
|
|
||||||
|
# For accounting purposes, record the balances |
||||||
|
# at the start. |
||||||
|
msb = getbals(wallets[0], 0) |
||||||
|
tsb = getbals(wallets[1], 0) |
||||||
|
|
||||||
|
cj_amount = int(1.1 * 10**8) |
||||||
|
maker = P2EPMaker(wallets[0], 0, cj_amount) |
||||||
|
destaddr = maker.destination_addr |
||||||
|
monkeypatch.setattr(maker, 'user_check', dummy_user_check) |
||||||
|
# TODO use this to sanity check behaviour |
||||||
|
# in presence of the rest of the joinmarket orderbook. |
||||||
|
orderbook = create_orderbook([maker]) |
||||||
|
assert len(orderbook) == 1 |
||||||
|
# mixdepth, amount, counterparties, dest_addr, waittime; |
||||||
|
# in payjoin we only pay attention to the first two entries. |
||||||
|
schedule = [(0, cj_amount, 1, destaddr, 0)] |
||||||
|
taker = create_taker(wallets[-1], schedule, monkeypatch) |
||||||
|
monkeypatch.setattr(taker, 'user_check', dummy_user_check) |
||||||
|
init_data = taker.initialize(orderbook) |
||||||
|
# the P2EPTaker.initialize() returns: |
||||||
|
# (True, self.cjamount, "p2ep", "p2ep", {self.p2ep_receiver_nick:{}}) |
||||||
|
assert init_data[0], "taker.initialize error" |
||||||
|
active_orders = init_data[4] |
||||||
|
assert len(active_orders.keys()) == 1 |
||||||
|
response = taker.receive_utxos(list(active_orders.keys())) |
||||||
|
assert response[0], "taker receive_utxos error" |
||||||
|
# test for validity of signed fallback transaction; requires 0.17; |
||||||
|
# note that we count this as an implicit test of fallback mode. |
||||||
|
res = jm_single().bc_interface.rpc('testmempoolaccept', [[response[2]]]) |
||||||
|
assert res[0]["allowed"], "Proposed transaction was rejected from mempool." |
||||||
|
maker_response = maker.on_tx_received("faketaker", response[2]) |
||||||
|
if not maker_response[0]: |
||||||
|
print("maker on_tx_received failed, reason: ", maker_response[1]) |
||||||
|
assert False |
||||||
|
taker_response = taker.on_tx_received("fakemaker", maker_response[2]) |
||||||
|
if not taker_response[1] == "OK": |
||||||
|
print("Failure in taker on_tx_received, reason: ", taker_response[1]) |
||||||
|
assert False |
||||||
|
# Although the above OK is proof that a transaction went through, |
||||||
|
# it doesn't prove it was a good transaction! Here do balance checks: |
||||||
|
assert final_checks(wallets, cj_amount, taker.total_txfee, tsb, msb) |
||||||
|
|
||||||
|
@pytest.fixture(scope='module') |
||||||
|
def setup_cj(): |
||||||
|
load_program_config() |
||||||
|
jm_single().config.set('POLICY', 'tx_broadcast', 'self') |
||||||
|
jm_single().bc_interface.tick_forward_chain_interval = 5 |
||||||
|
jm_single().bc_interface.simulate_blocks() |
||||||
|
#see note in cryptoengine.py: |
||||||
|
cryptoengine.BTC_P2WPKH.VBYTE = 100 |
||||||
|
yield None |
||||||
|
# teardown |
||||||
|
for dc in reactor.getDelayedCalls(): |
||||||
|
dc.cancel() |
||||||
@ -0,0 +1,88 @@ |
|||||||
|
#! /usr/bin/env python |
||||||
|
from __future__ import (absolute_import, division, |
||||||
|
print_function, unicode_literals) |
||||||
|
from builtins import * # noqa: F401 |
||||||
|
|
||||||
|
from optparse import OptionParser |
||||||
|
|
||||||
|
import sys |
||||||
|
from twisted.python.log import startLogging |
||||||
|
from jmbase import get_log, set_logging_level |
||||||
|
from jmclient import P2EPMaker, jm_single, load_program_config, \ |
||||||
|
sync_wallet, JMClientProtocolFactory, start_reactor, \ |
||||||
|
open_test_wallet_maybe, get_wallet_path |
||||||
|
from cli_options import check_regtest |
||||||
|
|
||||||
|
jlog = get_log() |
||||||
|
|
||||||
|
def receive_payjoin_main(makerclass): |
||||||
|
parser = OptionParser(usage='usage: %prog [options] [wallet file] [amount-to-receive]') |
||||||
|
parser.add_option('-g', '--gap-limit', action='store', type="int", |
||||||
|
dest='gaplimit', default=6, |
||||||
|
help='gap limit for wallet, default=6') |
||||||
|
parser.add_option('--fast', |
||||||
|
action='store_true', |
||||||
|
dest='fastsync', |
||||||
|
default=False, |
||||||
|
help=('choose to do fast wallet sync, only for Core and ' |
||||||
|
'only for previously synced wallet')) |
||||||
|
parser.add_option('-m', '--mixdepth', action='store', type='int', |
||||||
|
dest='mixdepth', default=0, |
||||||
|
help="mixdepth to source coins from") |
||||||
|
parser.add_option('-a', |
||||||
|
'--amtmixdepths', |
||||||
|
action='store', |
||||||
|
type='int', |
||||||
|
dest='amtmixdepths', |
||||||
|
help='number of mixdepths in wallet, default 5', |
||||||
|
default=5) |
||||||
|
(options, args) = parser.parse_args() |
||||||
|
if len(args) < 2: |
||||||
|
parser.error('Needs a wallet, and a receiving amount in satoshis') |
||||||
|
sys.exit(0) |
||||||
|
wallet_name = args[0] |
||||||
|
try: |
||||||
|
receiving_amount = int(args[1]) |
||||||
|
except: |
||||||
|
parser.error("Invalid receiving amount passed: " + receiving_amount) |
||||||
|
sys.exit(0) |
||||||
|
if receiving_amount < 0: |
||||||
|
parser.error("Receiving amount must be a positive integer in satoshis") |
||||||
|
sys.exit(0) |
||||||
|
load_program_config() |
||||||
|
|
||||||
|
check_regtest() |
||||||
|
|
||||||
|
# This workflow requires command line reading; we force info level logging |
||||||
|
# to remove noise, and mostly communicate to the user with the fn |
||||||
|
# log.info (via P2EPMaker.user_info). |
||||||
|
set_logging_level("INFO") |
||||||
|
|
||||||
|
wallet_path = get_wallet_path(wallet_name, 'wallets') |
||||||
|
max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1]) |
||||||
|
wallet = open_test_wallet_maybe( |
||||||
|
wallet_path, wallet_name, max_mix_depth, |
||||||
|
gap_limit=options.gaplimit) |
||||||
|
|
||||||
|
if jm_single().config.get("BLOCKCHAIN", "blockchain_source") == "electrum-server": |
||||||
|
jm_single().bc_interface.synctype = "with-script" |
||||||
|
|
||||||
|
while not jm_single().bc_interface.wallet_synced: |
||||||
|
sync_wallet(wallet, fast=options.fastsync) |
||||||
|
|
||||||
|
maker = makerclass(wallet, options.mixdepth, receiving_amount) |
||||||
|
|
||||||
|
jlog.info('starting receive-payjoin') |
||||||
|
clientfactory = JMClientProtocolFactory(maker, proto_type="MAKER") |
||||||
|
|
||||||
|
nodaemon = jm_single().config.getint("DAEMON", "no_daemon") |
||||||
|
daemon = True if nodaemon == 1 else False |
||||||
|
if jm_single().config.get("BLOCKCHAIN", "network") in ["regtest", "testnet"]: |
||||||
|
startLogging(sys.stdout) |
||||||
|
start_reactor(jm_single().config.get("DAEMON", "daemon_host"), |
||||||
|
jm_single().config.getint("DAEMON", "daemon_port"), |
||||||
|
clientfactory, daemon=daemon, p2ep=True) |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
receive_payjoin_main(P2EPMaker) |
||||||
|
print('done') |
||||||
Loading…
Reference in new issue