Browse Source

Merge #544: Fidelity bond wallets

6b41b8b Disable creation of fidelity bond wallets (chris-belcher)
869ef55 Disable loading of fidelity bond wallets by Qt (chris-belcher)
14f086b Add usage guide for fidelity bond wallets (chris-belcher)
2860c4f Freeze timelocked UTXOs with locktimes in future (chris-belcher)
ddb32ce Rename functions to say "key" instead of "privkey" (chris-belcher)
c70183b Create tests for fidelity bond wallets (chris-belcher)
a0a0d28 Add support for spending timelocked UTXOs (chris-belcher)
762b1f6 Add watch only wallets for fidelity bonds (chris-belcher)
a937c44 Add wallet-tool addtxoutproof method (chris-belcher)
97216d3 Sync burner outputs and display in wallet-tool (chris-belcher)
255d155 Add merkle proof functions to BitcoinCoreInterface (chris-belcher)
2271ce0 Add support for burning coins with sendpayment (chris-belcher)
dc715c9 Add timelock fidelity bond wallet sync and display (chris-belcher)
ee70cd7 Add support for OP_CLTV timelock addresses (chris-belcher)
d86df33 Rename functions which create multisig scripts (chris-belcher)
53b056e Rename variable internal to address_type (chris-belcher)

Tree-SHA512: 9f24000b0ebb4524b30c8afaa4416e516c7241c565dd21a96f83b70d6b6c42e83ad20bfd4f6e85545a855af4fcfd6065feb674f5ed270ca51ac814b4c3684ab4
master
chris-belcher 6 years ago
parent
commit
c1f34f08c5
No known key found for this signature in database
GPG Key ID: EF734EA677F31129
  1. 271
      docs/fidelity-bonds.md
  2. 2
      jmbitcoin/jmbitcoin/secp256k1_deterministic.py
  3. 2
      jmbitcoin/jmbitcoin/secp256k1_main.py
  4. 85
      jmbitcoin/jmbitcoin/secp256k1_transaction.py
  5. 12
      jmclient/jmclient/__init__.py
  6. 25
      jmclient/jmclient/blockchaininterface.py
  7. 2
      jmclient/jmclient/cli_options.py
  8. 5
      jmclient/jmclient/configure.py
  9. 138
      jmclient/jmclient/cryptoengine.py
  10. 84
      jmclient/jmclient/taker_utils.py
  11. 505
      jmclient/jmclient/wallet.py
  12. 137
      jmclient/jmclient/wallet_service.py
  13. 293
      jmclient/jmclient/wallet_utils.py
  14. 10
      jmclient/test/commontest.py
  15. 52
      jmclient/test/test_tx_creation.py
  16. 163
      jmclient/test/test_wallet.py
  17. 8
      scripts/joinmarket-qt.py
  18. 11
      scripts/sendpayment.py

271
docs/fidelity-bonds.md

@ -0,0 +1,271 @@
# Fidelity bonds
Fidelity bonds are a feature of JoinMarket which improves the resistance to
sybil attacks, and therefore improves the privacy of the system.
## This feature is incomplete and so is disabled for now
A fidelity bond is a mechanism where bitcoin value is deliberately sacrificed
to make a cryptographic identity expensive to obtain. The sacrifice is done in
a way that can be proven to a third party. Takers in JoinMarket will
have a higher probability to create coinjoins with makers who publish more
valuable fidelity bonds. This has the effect of making the system much more
expensive to sybil attack, because an attacker would have to sacrifice a lot of
value in order to be chosen very often by takers when creating coinjoin.
As a maker you can take part in many more coinjoins and therefore earn more
fees if you sacrifice bitcoin value to create a fidelity bond. The most
practical way to create a fidelity bond is to send bitcoins to a time-locked
address which uses the opcode [OP_CHECKLOCKTIMEVERIFY](https://en.bitcoin.it/wiki/Timelock#CheckLockTimeVerify).
The valuable thing being sacrificed is the time-value-of-money. Note that a
long-term holder (or hodler) of bitcoins can buy time-locked fidelity bonds
essentially for free, assuming they never intended to transact with their coins
anyway.
Another way to create fidelity bonds is to destroy coins by sending them to a
[OP_RETURN](https://en.bitcoin.it/wiki/Script#Provably_Unspendable.2FPrunable_Outputs)
output.
The private keys to fidelity bonds can be kept in [cold storage](https://en.bitcoin.it/wiki/Cold_storage)
for added security.
For a more detailed explanation of how fidelity bonds work see these documents:
* [Design for improving JoinMarket's resistance to sybil attacks using fidelity
bonds](https://gist.github.com/chris-belcher/18ea0e6acdb885a2bfbdee43dcd6b5af/)
* [Financial mathematics of JoinMarket fidelity bonds](https://gist.github.com/chris-belcher/87ebbcbb639686057a389acb9ab3e25b)
#### Note on privacy
Bitcoin outputs which create fidelity bonds will be published to the entire
world, so before and after creating them make sure the outputs are not linked
to your identity in any way. Perhaps mix with JoinMarket before and after.
### Creating a JoinMarket wallet which supports fidelity bonds
When generating a JoinMarket wallet on the command line, supporting versions
will offer an option to make the wallet support fidelity bonds.
(jmvenv) $ python3 wallet-tool.py generate
Would you like to use a two-factor mnemonic recovery phrase? write 'n' if you don't know what this is (y/n): n
Not using mnemonic extension
Enter wallet file encryption passphrase:
Reenter wallet file encryption passphrase:
Input wallet file name (default: wallet.jmdat): testfidelity.jmdat
Would you like this wallet to support fidelity bonds? write 'n' if you don't know what this is (y/n): y
Write down this wallet recovery mnemonic
evidence initial knee image inspire plug dad midnight blast awkward clean between
Generated wallet OK
As always, it is crucially important to write down the 12-word [seed phrase](https://en.bitcoin.it/wiki/Seed_phrase)
as a backup. It is also recommended to write down the name of the creating wallet
"JoinMarket" and that the fidelity bond option was enabled. Writing the wallet
creation date is also useful as it can help with rescanning.
### Obtaining time-locked addresses
The `wallet-tool.py` script supports a new method `gettimelockaddress` used for
obtaining time-locked addresses. If coins are sent to these addresses they will
be locked up until the timelock date passes. Only mixdepth zero can have
fidelity bonds in it.
This example creates an address which locks any coins sent to it until April 2020.
(jmvenv) $ python3 wallet-tool.py testfidelity.jmdat gettimelockaddress 2020-4
Enter wallet decryption passphrase:
path = m/49'/1'/0'/2/3:1585699200
Coins sent to this address will be not be spendable until April 2020. Full date: 2020-04-01 00:00:00
bcrt1qrc2qu3m2l2spayu5kr0k0rnn9xgjz46zsxmruh87a3h3f5zmnkaqlfx7v5
If coins are sent to these addresses they will appear in the usual `wallet-tool.py`
display:
(jmvenv) $ python3 wallet-tool.py -m 0 testfidelity.jmdat
Enter wallet decryption passphrase:
JM wallet
mixdepth 0 fbonds-mpk-tpubDDCbCPdf5wJVGYWB4mZr3E3Lys4NBcEKysrrUrLfhG6sekmrvs6KZNe4i5p5z3FyfwRmKMqB9NWEcEUiTS4LwqfrKPQzhKj6aLihu2EejaU
external addresses m/49'/1'/0'/0 tpubDEGdmPwmQRcZmGKhaudjch9Fgw4J5yP4bYw5B8LoSDkMdmhBxM4ndEQXHK4r1TPexGjLidxdpeEzsJcdXEe7khWToxCZuN6JiLzvUoHAki2
m/49'/1'/0'/0/0 2N8jHuQaApgFtQ8UKxKbREAvNxKn4BGX4x2 0.00000000 new
m/49'/1'/0'/0/1 2Mx5CwDoNcuCT38EDmgenQxv9skHbZfXFdo 0.00000000 new
m/49'/1'/0'/0/2 2N1tNTTwNyucGGmfDWNVk3AUi3i5S8jVKqn 0.00000000 new
m/49'/1'/0'/0/3 2N8eBEU5wpWb6kS1gvbRgewtxsmXsMkShV6 0.00000000 new
m/49'/1'/0'/0/4 2MuHgeSgMsvkcn6aGNW2uk2UXP3xVVnkfh2 0.00000000 new
m/49'/1'/0'/0/5 2NA8d8um5KmBNNR8dadhbEDYGiTJPFCdjMB 0.00000000 new
Balance: 0.00000000
internal addresses m/49'/1'/0'/1
Balance: 0.00000000
internal addresses m/49'/1'/0'/2 tpubDEGdmPwmQRcZrzjRmUFqXXyLdRedwxCWQviAFqDe6sXJeZzRNTwmwqMfxN6Ka3v7hEebstrU5kqUNoHsFKaA3RoB2vopL6kLHVo1EQq6USw
m/49'/1'/0'/2/0:1585699200 bcrt1qrc2qu3m2l2spayu5kr0k0rnn9xgjz46zsxmruh87a3h3f5zmnkaqlfx7v5 0.15000000 2020-04-01 [LOCKED]
Balance: 0.15000000
Balance for mixdepth 0: 0.15000000
Total balance: 0.15000000
#### Spending time-locked coins
Once the time-lock of an address expires the coins can be spent with JoinMarket.
Coins living on time-locked addresses are automatically frozen with
JoinMarket's coin control feature, so before spending you need to unfreeze the
coins using `python3 wallet-tool.py <walletname> -m 0 freeze`.
Once unfrozen and untimelocked the coins can be spent normally with the scripts
`sendpayment.py`, `tumber.py`, or yield generator.
### Burning coins
Coins can be burned with a special method of the `sendpayment.py` script. Set
the destination to be `BURN`. Transactions which burn coins must only have one
input and one output, so use coin control to freeze all coins in the zeroth
mixdepth except one.
$ python3 sendpayment.py -N 0 testfidelity3.jmdat 0 BURN
User data location: .
2020-04-07 20:45:25,658 [INFO] Using this min relay fee as tx fee floor: 1000 sat/vkB (1.0 sat/vB)
Enter wallet decryption passphrase:
2020-04-07 20:46:50,449 [INFO] Estimated miner/tx fees for this coinjoin amount: 0.0%
2020-04-07 20:46:50,452 [INFO] Using this min relay fee as tx fee floor: 1000 sat/vkB (1.0 sat/vB)
2020-04-07 20:46:50,452 [INFO] Using a fee of : 0.00000200 BTC (200 sat).
2020-04-07 20:46:50,454 [INFO] Got signed transaction:
2020-04-07 20:46:50,455 [INFO] {'ins': [{'outpoint': {'hash': '61d69b4e7abe0ef8a5a9cbabb05463259c3b497a142130a56f81a9259f048cb0',
'index': 0},
'script': '160014295beb4eba9b35896683d5b5ff455ee2c646054c',
'sequence': 4294967294,
'txinwitness': ['3045022100939de908e30015c6b22d2c0f25153e395268466ce44eeeb4ec03a8920440e87b0220155d1c43dedb3fc2654205541bb2821dd5211180e2d7f93d67301470652830d401',
'03ec0f8b267f99ff5259195ce63813d58f38ffbaada894ce06af0c0303c74bbf82']}],
'locktime': 1361,
'outs': [{'script': '6a147631c805d8ad9239677b8d7530353fda3fec07ca',
'value': 11999800}],
'version': 2}
2020-04-07 20:46:50,455 [INFO] In serialized form (for copy-paste):
2020-04-07 20:46:50,455 [INFO] 02000000000101b08c049f25a9816fa53021147a493b9c256354b0abcba9a5f80ebe7a4e9bd6610000000017160014295beb4eba9b35896683d5b5ff455ee2c646054cfeffffff01381ab70000000000166a147631c805d8ad9239677b8d7530353fda3fec07ca02483045022100939de908e30015c6b22d2c0f25153e395268466ce44eeeb4ec03a8920440e87b0220155d1c43dedb3fc2654205541bb2821dd5211180e2d7f93d67301470652830d4012103ec0f8b267f99ff5259195ce63813d58f38ffbaada894ce06af0c0303c74bbf8251050000
2020-04-07 20:46:50,456 [INFO] Sends: 0.11999800 BTC (11999800 sat) to destination: BURNER OUTPUT embedding pubkey at m/49'/1'/0'/3/0
WARNING: This transaction if broadcasted will PERMANENTLY DESTROY your bitcoins
Would you like to push to the network? (y/n):y
2020-04-07 20:47:52,047 [DEBUG] rpc: sendrawtransaction ['02000000000101b08c049f25a9816fa53021147a493b9c256354b0abcba9a5f80ebe7a4e9bd6610000000017160014295beb4eba9b35896683d5b5ff455ee2c646054cfeffffff01381ab70000000000166a147631c805d8ad9239677b8d7530353fda3fec07ca02483045022100939de908e30015c6b22d2c0f25153e395268466ce44eeeb4ec03a8920440e87b0220155d1c43dedb3fc2654205541bb2821dd5211180e2d7f93d67301470652830d4012103ec0f8b267f99ff5259195ce63813d58f38ffbaada894ce06af0c0303c74bbf8251050000']
2020-04-07 20:47:52,049 [WARNING] Connection had broken pipe, attempting reconnect.
2020-04-07 20:47:52,440 [INFO] Transaction sent: 656bb4538f14f2cc874043915907b6c9c46a807ef9818bde771d07630d54b0f7
done
Embedded in the `OP_RETURN` output is the hash of a pubkey from the wallet.
Now `OP_RETURN` outputs are not addresses, and because of technical reasons the
first time they are synchronized the flag `--recoversync` must be used. When
this is done the burn output will appear in the `wallet-tool.py` display.
`--recoversync` only needs to be used once, and after that the burner output is
saved in the `wallet.jmdat` file which can be accesses by all future
synchronizations.
$ python3 wallet-tool.py --datadir=. --recoversync testfidelity2.jmdat
Enter wallet decryption passphrase:
2020-04-07 23:09:54,644 [INFO] Found a burner transaction txid=656bb4538f14f2cc874043915907b6c9c46a807ef9818bde771d07630d54b0f7 path = m/49'/1'/0'/3/0
2020-04-07 23:09:54,769 [WARNING] Merkle branch not available, use wallet-tool `addtxoutproof`
2020-04-07 23:09:55,420 [INFO] Found a burner transaction txid=656bb4538f14f2cc874043915907b6c9c46a807ef9818bde771d07630d54b0f7 path = m/49'/1'/0'/3/0
2020-04-07 23:09:55,422 [WARNING] Merkle branch not available, use wallet-tool `addtxoutproof`
JM wallet
mixdepth 0 fbonds-mpk-tpubDDCXgSpdxuVbXgzRYBggFeMRNeV9eH24jJuQNunyqwYtDFiB7ZS63LhXwHkf7o9ZcZW4qUz7uvD6yk4BkkF3bBPmJRPv7RBTEA5hHwEdV2f
external addresses m/49'/1'/0'/0 tpubDEJGorVywRb6LoLQbaqWZh2gYwpdZqViCNZ2ejB5kpBuUp16LHpK6ESFaJLixidtbmmjcDwVZ4QjnAbKmypfuGaEk3ifgonPv4vsugqgp9G
m/49'/1'/0'/0/2 2MviB2FfLKZjFb3W2dJ8kXcQBj3jqMJg7TL 0.00000000 new
m/49'/1'/0'/0/3 2MtsAQhE2u9VGV3aZ7XM4PzwWGHXr4PAhqP 0.00000000 new
m/49'/1'/0'/0/4 2N3iXNjS4vkTXzy5Jidnovc6FJeNQJx5Fnt 0.00000000 new
m/49'/1'/0'/0/5 2MtT4XAjwQQ7PBbrTxv7qcQMMmz4Rs5XrE3 0.00000000 new
m/49'/1'/0'/0/6 2NEuG23BQESuZTqSDtab9zYsd1Jb4KfMULB 0.00000000 new
m/49'/1'/0'/0/7 2N6NGJRX6KYQWtYWK8iHFuJNpZRs8NbUAC9 0.00000000 new
Balance: 0.00000000
internal addresses m/49'/1'/0'/1
Balance: 0.00000000
internal addresses m/49'/1'/0'/2 tpubDEJGorVywRb6T34X7ZAEz9hQYn6CCEhrcFa8kA2mqNau2DvoggZP2QTtXRe8t9NSfMkx3ye8QDzqCE9gEqso6fw5ALk5xycWLFwTRLSqSUV
Balance: 0.00000000
internal addresses m/49'/1'/0'/3 tpubDEJGorVywRb6V5em9Q7LFJ9eLEAEZmZxUdDmkknrKNUs7vKcCWPKwP8YPjuxFCCXk2F1wJnubNbmgbtWed5yiE3D1qxzLonVuXT6QEZPaof
m/49'/1'/0'/3/0 BURN-7631c805d8ad9239677b8d7530353fda3fec07ca 0.11999800 656bb4538f14f2cc874043915907b6c9c46a807ef9818bde771d07630d54b0f7 [NO MERKLE PROOF]
Balance: 0.11999800
Balance for mixdepth 0: 0.11999800
Total balance: 0.11999800
#### Adding the merkle proof of a burn transaction if missing
In order to prove a burn output exists, a merkle proof is needed. If the Core
node is pruned and the block deleted then JoinMarket will not be able to obtain
the merkle proof (as in the above example). In this case the proof can be
added separately.
Any other unpruned Core node can trustlessly obtain the proof and give it to
the user with the RPC call `gettxoutproof`.
First obtain the merkle proof:
$ bitcoin-cli gettxoutproof "[\"656bb4538f14f2cc874043915907b6c9c46a807ef9818bde771d07630d54b0f7\"]" 4cce28f4eb1ea1762ec4ceb90529b3ab28c0423ac630c6292319e2b2712daada
0000002056e4050f54084a1d6e6944b209cce76ebe2da4b37f3aa47ab6c612831d3220471015e80d3050cf0ee05b216036030fe3d4906221196943a30e828741ad4cfaeb05d98c5effff7f20010000000200000002a5910e5cf4e6cb6d55e1e2ca979987772a482e8a8f30b7a6cab8c5671f5c161df7b0540d63071d77de8b81f97e806ac4c9b6075991434087ccf2148f53b46b650105
Then add it to the JoinMarket wallet:
$ python3 wallet-tool.py -H "m/49'/1'/0'/3/0" testfidelity2.jmdat addtxoutproof 0000002056e4050f54084a1d6e6944b209cce76ebe2da4b37f3aa47ab6c612831d3220471015e80d3050cf0ee05b216036030fe3d4906221196943a30e828741ad4cfaeb05d98c5effff7f20010000000200000002a5910e5cf4e6cb6d55e1e2ca979987772a482e8a8f30b7a6cab8c5671f5c161df7b0540d63071d77de8b81f97e806ac4c9b6075991434087ccf2148f53b46b650105
Enter wallet decryption passphrase:
Done
The `-H` flag must point to the path containing the burn output.
Then synchronizing the wallet won't output the no-merkle-proof warning.
### Creating watch-only fidelity bond wallets
Fidelity bonds can be held on an offline computer in
[cold storage](https://en.bitcoin.it/wiki/Cold_storage). To do this we create
a watch-only fidelity bond wallet.
When fidelity bonds are displayed in `wallet-tool.py`, their master public key
is highlighted with a prefix `fbonds-mpk-`.
This master public key can be used to create a watch-only wallet using
`wallet-tool.py`.
$ python3 wallet-tool.py createwatchonly fbonds-mpk-tpubDDCbCPdf5wJVGYWB4mZr3E3Lys4NBcEKysrrUrLfhG6sekmrvs6KZNe4i5p5z3FyfwRmKMqB9NWEcEUiTS4LwqfrKPQzhKj6aLihu2EejaU
Input wallet file name (default: watchonly.jmdat): watchfidelity.jmdat
Enter wallet file encryption passphrase:
Reenter wallet file encryption passphrase:
Done
Then the wallet can be displayed like a regular wallet, although only the zeroth
mixdepth will be shown.
$ python3 wallet-tool.py watchfidelity.jmdat
User data location: .
Enter wallet decryption passphrase:
JM wallet
mixdepth 0 fbonds-mpk-tpubDDCbCPdf5wJVGYWB4mZr3E3Lys4NBcEKysrrUrLfhG6sekmrvs6KZNe4i5p5z3FyfwRmKMqB9NWEcEUiTS4LwqfrKPQzhKj6aLihu2EejaU
external addresses m/49'/1'/0'/0 tpubDEGdmPwmQRcZmGKhaudjch9Fgw4J5yP4bYw5B8LoSDkMdmhBxM4ndEQXHK4r1TPexGjLidxdpeEzsJcdXEe7khWToxCZuN6JiLzvUoHAki2
m/49'/1'/0'/0/0 2N8jHuQaApgFtQ8UKxKbREAvNxKn4BGX4x2 0.00000000 used
m/49'/1'/0'/0/1 2Mx5CwDoNcuCT38EDmgenQxv9skHbZfXFdo 0.00000000 new
m/49'/1'/0'/0/2 2N1tNTTwNyucGGmfDWNVk3AUi3i5S8jVKqn 0.00000000 new
m/49'/1'/0'/0/3 2N8eBEU5wpWb6kS1gvbRgewtxsmXsMkShV6 0.00000000 new
m/49'/1'/0'/0/4 2MuHgeSgMsvkcn6aGNW2uk2UXP3xVVnkfh2 0.00000000 new
m/49'/1'/0'/0/5 2NA8d8um5KmBNNR8dadhbEDYGiTJPFCdjMB 0.00000000 new
m/49'/1'/0'/0/6 2NG76BAHPccfyy6sH68EHrB9QJBycx3FKb6 0.00000000 new
Balance: 0.25000000
internal addresses m/49'/1'/0'/1
Balance: 0.00000000
internal addresses m/49'/1'/0'/2 tpubDEGdmPwmQRcZrzjRmUFqXXyLdRedwxCWQviAFqDe6sXJeZzRNTwmwqMfxN6Ka3v7hEebstrU5kqUNoHsFKaA3RoB2vopL6kLHVo1EQq6USw
m/49'/1'/0'/0/3:1585699200 bcrt1qrc2qu3m2l2spayu5kr0k0rnn9xgjz46zsxmruh87a3h3f5zmnkaqlfx7v5 0.15000000 2020-04-01 [UNLOCKED]
Balance: 0.15000000
internal addresses m/49'/1'/0'/3 tpubDEGdmPwmQRcZuX3uNrCouu5bRgp2GJcoQTvhkFAJMTA3yxhKmQyeGwecbnkms4DYmBhCJn2fGTuejTe3g8oyJW3qKcfB4b3Swj2hDk1h4Y2
Balance: 0.00000000
Balance for mixdepth 0: 0.15000000
Total balance: 0.15000000
### BIP32 Paths
Fidelity bond wallets extend the BIP32 path format to include the locktime
values. In this example we've got `m/49'/1'/0'/2/0:1583020800` where the
number after the colon is the locktime value in Unix time.
This path can be passed to certain wallet methods like `dumpprivkey`.
$ python3 wallet-tool.py -H "m/49'/1'/0'/2/0:1583020800" testfidelity.jmdat dumpprivkey
Enter wallet decryption passphrase:
cNEuE5ypNTxVFCyC5iH7u5AQTrddamcUHRPNweiLvmHUWd6XXDkz

2
jmbitcoin/jmbitcoin/secp256k1_deterministic.py

@ -65,6 +65,8 @@ def bip32_deserialize(data):
def raw_bip32_privtopub(rawtuple):
vbytes, depth, fingerprint, i, chaincode, key = rawtuple
if vbytes in PUBLIC:
return rawtuple
newvbytes = MAINNET_PUBLIC if vbytes == MAINNET_PRIVATE else TESTNET_PUBLIC
return (newvbytes, depth, fingerprint, i, chaincode, privtopub(key, False))

2
jmbitcoin/jmbitcoin/secp256k1_main.py

@ -10,7 +10,7 @@ import coincurve as secp256k1
#Required only for PoDLE calculation:
N = 115792089237316195423570985008687907852837564279074904382605163141518161494337
BTC_P2PK_VBYTE = {"mainnet": b'\x00', "testnet": b'\x6f'}
BTC_P2PK_VBYTE = {"mainnet": b'\x00', "testnet": b'\x6f', "regtest": 100}
BTC_P2SH_VBYTE = {"mainnet": b'\x05', "testnet": b'\xc4'}
#Standard prefix for Bitcoin message signing.

85
jmbitcoin/jmbitcoin/secp256k1_transaction.py

@ -10,6 +10,7 @@ import struct
import random
from jmbitcoin.secp256k1_main import *
from jmbitcoin.bech32 import *
import jmbitcoin as btc
P2PKH_PRE, P2PKH_POST = b'\x76\xa9\x14', b'\x88\xac'
P2SH_P2WPKH_PRE, P2SH_P2WPKH_POST = b'\xa9\x14', b'\x87'
@ -534,23 +535,29 @@ def pubkey_to_p2wpkh_address(pub):
script = pubkey_to_p2wpkh_script(pub)
return script_to_address(script)
def pubkeys_to_p2wsh_script(pubs):
def pubkeys_to_p2wsh_multisig_script(pubs):
""" Given a list of N pubkeys, constructs an N of N
multisig scriptPubKey of type pay-to-witness-script-hash.
No other scripts than N-N multisig supported as of now.
"""
N = len(pubs)
script = mk_multisig_script(pubs, N)
return P2WSH_PRE + bin_sha256(binascii.unhexlify(script))
return redeem_script_to_p2wsh_script(script)
def pubkeys_to_p2wsh_address(pubs):
def pubkeys_to_p2wsh_multisig_address(pubs):
""" Given a list of N pubkeys, constructs an N of N
multisig address of type pay-to-witness-script-hash.
No other scripts than N-N multisig supported as of now.
"""
script = pubkeys_to_p2wsh_script(pubs)
script = pubkeys_to_p2wsh_multisig_script(pubs)
return script_to_address(script)
def redeem_script_to_p2wsh_script(redeem_script):
return P2WSH_PRE + bin_sha256(binascii.unhexlify(redeem_script))
def redeem_script_to_p2wsh_address(redeem_script, vbyte, witver=0):
return script_to_address(redeem_script_to_p2wsh_script(redeem_script), vbyte, witver)
def deserialize_script(scriptinp):
""" Note that this is not used internally, in
the jmbitcoin package, to deserialize() transactions;
@ -603,10 +610,14 @@ def deserialize_script(scriptinp):
def serialize_script_unit(unit):
if isinstance(unit, int):
if unit < 16:
if unit == 0:
return from_int_to_byte(unit)
elif unit < 16:
return from_int_to_byte(unit + 80)
else:
elif unit < 256:
return from_int_to_byte(unit)
else:
return b'\x04' + struct.pack(b"<I", unit)
elif unit is None:
return b'\x00'
else:
@ -634,15 +645,37 @@ def serialize_script(script):
else:
return result
def mk_multisig_script(pubs, k):
""" Given a list of pubkeys and an integer k,
construct a multisig script for k of N, where N is
the length of the list `pubs`; script is returned
as hex string.
"""
return serialize_script([k] + pubs + [len(pubs)]) + 'ae'
return serialize_script([k] + pubs + [len(pubs)]) \
+ 'ae' #OP_CHECKMULTISIG
def mk_freeze_script(pub, locktime):
"""
Given a pubkey and locktime, create a script which can only be spent
after the locktime has passed using OP_CHECKLOCKTIMEVERIFY
"""
if not isinstance(locktime, int):
raise TypeError("locktime must be int")
if not isinstance(pub, bytes):
raise TypeError("pubkey must be in bytes")
usehex = False
if not is_valid_pubkey(pub, usehex, require_compressed=True):
raise ValueError("not a valid public key")
scr = [locktime, btc.OP_CHECKLOCKTIMEVERIFY, btc.OP_DROP, pub,
btc.OP_CHECKSIG]
return binascii.hexlify(serialize_script(scr)).decode()
def mk_burn_script(data):
if not isinstance(data, bytes):
raise TypeError("data must be in bytes")
data = binascii.hexlify(data).decode()
scr = [btc.OP_RETURN, data]
return serialize_script(scr)
# Signing and verifying
@ -701,8 +734,8 @@ def sign(tx, i, priv, hashcode=SIGHASH_ALL, usenonce=None, amount=None,
`amount` flags whether segwit signing is to be done, and the field
`native` flags that native segwit p2wpkh signing is to be done. Note
that signing multisig is to be done with the alternative functions
multisign or p2wsh_multisign (and non N of N multisig scripthash
signing is not currently supported).
get_p2sh_signature or get_p2wsh_signature (and non N of N multisig
scripthash signing is not currently supported).
"""
if isinstance(tx, basestring) and not isinstance(tx, bytes):
tx = binascii.unhexlify(tx)
@ -762,28 +795,28 @@ def signall(tx, priv):
tx = sign(tx, i, priv)
return tx
def multisign(tx, i, script, pk, amount=None, hashcode=SIGHASH_ALL):
""" Tx is assumed to be serialized. The script passed here is
the redeemscript, for example the output of mk_multisig_script.
def get_p2sh_signature(tx, i, redeem_script, pk, amount=None, hashcode=SIGHASH_ALL):
"""
Tx is assumed to be serialized. redeem_script is for example the
output of mk_multisig_script.
pk is the private key, and must be passed in hex.
If amount is not None, the output of p2wsh_multisign is returned.
If amount is not None, the output of get_p2wsh_signature is returned.
What is returned is a single signature.
"""
if isinstance(tx, str):
tx = binascii.unhexlify(tx)
if isinstance(script, str):
script = binascii.unhexlify(script)
if isinstance(redeem_script, str):
redeem_script = binascii.unhexlify(redeem_script)
if amount:
return p2wsh_multisign(tx, i, script, pk, amount, hashcode)
modtx = signature_form(tx, i, script, hashcode)
return get_p2wsh_signature(tx, i, redeem_script, pk, amount, hashcode)
modtx = signature_form(tx, i, redeem_script, hashcode)
return ecdsa_tx_sign(modtx, pk, hashcode)
def p2wsh_multisign(tx, i, script, pk, amount, hashcode=SIGHASH_ALL):
def get_p2wsh_signature(tx, i, redeem_script, pk, amount, hashcode=SIGHASH_ALL):
""" See note to multisign for the value to pass in as `script`.
Tx is assumed to be serialized.
"""
modtx = segwit_signature_form(deserialize(tx), i, script, amount,
modtx = segwit_signature_form(deserialize(tx), i, redeem_script, amount,
hashcode, decoder_func=lambda x: x)
return ecdsa_tx_sign(modtx, pk, hashcode)
@ -819,6 +852,16 @@ def apply_multisignatures(*args):
txobj["ins"][i]["script"] = serialize_script([None] + sigs + [script])
return serialize(txobj)
def apply_freeze_signature(tx, i, redeem_script, sig):
if isinstance(redeem_script, str):
redeem_script = binascii.unhexlify(redeem_script)
if isinstance(sig, str):
sig = binascii.unhexlify(sig)
txobj = deserialize(tx)
txobj["ins"][i]["script"] = ""
txobj["ins"][i]["txinwitness"] = [sig, redeem_script]
return serialize(txobj)
def mktx(ins, outs, version=1, locktime=0):
""" Given a list of input dicts with key "output"
which are txid:n strings in hex, and a list of outputs

12
jmclient/jmclient/__init__.py

@ -13,15 +13,17 @@ from .old_mnemonic import mn_decode, mn_encode
from .taker import Taker, P2EPTaker
from .wallet import (Mnemonic, estimate_tx_fee, WalletError, BaseWallet, ImportWalletMixin,
BIP39WalletMixin, BIP32Wallet, BIP49Wallet, LegacyWallet,
SegwitWallet, SegwitLegacyWallet, UTXOManager,
WALLET_IMPLEMENTATIONS, compute_tx_locktime)
SegwitWallet, SegwitLegacyWallet, FidelityBondMixin,
FidelityBondWatchonlyWallet, SegwitLegacyWalletFidelityBonds,
UTXOManager, WALLET_IMPLEMENTATIONS, compute_tx_locktime)
from .storage import (Argon2Hash, Storage, StorageError, RetryableStorageError,
StoragePasswordError, VolatileStorage)
from .cryptoengine import BTCEngine, BTC_P2PKH, BTC_P2SH_P2WPKH, EngineError
from .configure import (load_test_config,
load_program_config, get_p2pk_vbyte, jm_single, get_network, update_persist_config,
validate_address, get_irc_mchannels, get_blockchain_interface_instance,
get_p2sh_vbyte, set_config, is_segwit_mode, is_native_segwit_mode)
validate_address, is_burn_destination, get_irc_mchannels,
get_blockchain_interface_instance, get_p2sh_vbyte, set_config, is_segwit_mode,
is_native_segwit_mode)
from .blockchaininterface import (BlockchainInterface,
RegtestBitcoinCoreInterface, BitcoinCoreInterface)
from .electruminterface import ElectrumInterface
@ -48,7 +50,7 @@ from .cli_options import (add_base_options, add_common_options,
from .wallet_utils import (
wallet_tool_main, wallet_generate_recover_bip39, open_wallet,
open_test_wallet_maybe, create_wallet, get_wallet_cls, get_wallet_path,
wallet_display, get_utxos_enabled_disabled)
wallet_display, get_utxos_enabled_disabled, wallet_gettimelockaddress)
from .wallet_service import WalletService
from .maker import Maker, P2EPMaker
from .yieldgenerator import YieldGenerator, YieldGeneratorBasic, ygmain

25
jmclient/jmclient/blockchaininterface.py

@ -4,6 +4,7 @@ import random
import sys
import time
from decimal import Decimal
import binascii
from twisted.internet import reactor, task
import jmbitcoin as btc
@ -406,6 +407,30 @@ class BitcoinCoreInterface(BlockchainInterface):
except JsonRpcError:
return self.rpc('getblock', [blockhash])['time']
def get_tx_merkle_branch(self, txid, blockhash=None):
if not blockhash:
tx = self.rpc("gettransaction", [txid])
if tx["confirmations"] < 1:
raise ValueError("Transaction not in block")
blockhash = tx["blockhash"]
try:
core_proof = self.rpc("gettxoutproof", [[txid], blockhash])
except JsonRpcError:
raise ValueError("Block containing transaction is pruned")
return self.core_proof_to_merkle_branch(core_proof)
def core_proof_to_merkle_branch(self, core_proof):
core_proof = binascii.unhexlify(core_proof)
#first 80 bytes of a proof given by core are just a block header
#so we can save space by replacing it with a 4-byte block height
return core_proof[80:]
def verify_tx_merkle_branch(self, txid, block_height, merkle_branch):
block_hash = self.rpc("getblockhash", [block_height])
core_proof = self.rpc("getblockheader", [block_hash, False]) + \
binascii.hexlify(merkle_branch).decode()
ret = self.rpc("verifytxoutproof", [core_proof])
return len(ret) == 1 and ret[0] == txid
class RegtestBitcoinCoreMixin():
"""

2
jmclient/jmclient/cli_options.py

@ -447,7 +447,7 @@ def get_tumbler_parser():
def get_sendpayment_parser():
parser = OptionParser(
usage=
'usage: %prog [options] wallet_file amount destaddr\n' +
'usage: %prog [options] wallet_file amount destination\n' +
' %prog [options] wallet_file bitcoin_uri',
description='Sends a single payment from a given mixing depth of your '
+

5
jmclient/jmclient/configure.py

@ -405,6 +405,11 @@ def validate_address(addr):
return True, 'address validated'
_BURN_DESTINATION = "BURN"
def is_burn_destination(destination):
return destination == _BURN_DESTINATION
def donation_address(reusable_donation_pubkey=None): #pragma: no cover
#Donation code currently disabled, so not tested.
if not reusable_donation_pubkey:

138
jmclient/jmclient/cryptoengine.py

@ -5,10 +5,15 @@ from collections import OrderedDict
import struct
import jmbitcoin as btc
from .configure import get_network
TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH, TYPE_P2SH_M_N = range(4)
from .configure import get_network, jm_single
#NOTE: before fidelity bonds and watchonly wallet, each of these types corresponded
# to one wallet type and one engine, not anymore
#with fidelity bond wallets and watchonly fidelity bond wallet, the wallet class
# can have two engines, one for single-sig addresses and the other for timelocked addresses
TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH, TYPE_P2SH_M_N, TYPE_TIMELOCK_P2WSH, \
TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS, TYPE_WATCHONLY_FIDELITY_BONDS, \
TYPE_WATCHONLY_TIMELOCK_P2WSH, TYPE_WATCHONLY_P2SH_P2WPKH = range(9)
NET_MAINNET, NET_TESTNET = range(2)
NET_MAP = {'mainnet': NET_MAINNET, 'testnet': NET_TESTNET}
WIF_PREFIX_MAP = {'mainnet': b'\x80', 'testnet': b'\xef'}
@ -129,6 +134,7 @@ class BTCEngine(object):
@classmethod
def derive_bip32_pub_export(cls, master_key, path):
#in the case of watchonly wallets this priv is actually a pubkey
priv = cls._walk_bip32_path(master_key, path)
return btc.bip32_serialize(btc.raw_bip32_privtopub(priv))
@ -149,7 +155,7 @@ class BTCEngine(object):
return key
@classmethod
def privkey_to_script(cls, privkey):
def key_to_script(cls, privkey):
pub = cls.privkey_to_pubkey(privkey)
return cls.pubkey_to_script(pub)
@ -159,7 +165,7 @@ class BTCEngine(object):
@classmethod
def privkey_to_address(cls, privkey):
script = cls.privkey_to_script(privkey)
script = cls.key_to_script(privkey)
return btc.script_to_address(script, cls.VBYTE)
@classmethod
@ -283,8 +289,124 @@ class BTC_P2WPKH(BTCEngine):
return btc.sign(btc.serialize(tx), index, privkey,
hashcode=hashcode, amount=amount, native=True)
class BTC_Timelocked_P2WSH(BTCEngine):
"""
In this class many instances of "privkey" or "pubkey" are actually tuples
of (privkey, timelock) or (pubkey, timelock)
"""
@classproperty
def VBYTE(cls):
#slight hack here, network can be either "mainnet" or "testnet"
#but we need to distinguish between actual testnet and regtest
if get_network() == "mainnet":
return btc.BTC_P2PK_VBYTE["mainnet"]
else:
if jm_single().config.get("BLOCKCHAIN", "blockchain_source")\
== "regtest":
return btc.BTC_P2PK_VBYTE["regtest"]
else:
assert get_network() == "testnet"
return btc.BTC_P2PK_VBYTE["testnet"]
@classmethod
def key_to_script(cls, privkey_locktime):
privkey, locktime = privkey_locktime
pub = cls.privkey_to_pubkey(privkey)
return cls.pubkey_to_script((pub, locktime))
@classmethod
def pubkey_to_script(cls, pubkey_locktime):
redeem_script = cls.pubkey_to_script_code(pubkey_locktime)
return btc.redeem_script_to_p2wsh_script(redeem_script)
@classmethod
def pubkey_to_script_code(cls, pubkey_locktime):
pubkey, locktime = pubkey_locktime
return btc.mk_freeze_script(pubkey, locktime)
@classmethod
def privkey_to_wif(cls, privkey_locktime):
priv, locktime = privkey_locktime
return btc.bin_to_b58check(priv, cls.WIF_PREFIX)
@classmethod
def sign_transaction(cls, tx, index, privkey_locktime, amount,
hashcode=btc.SIGHASH_ALL, **kwargs):
assert amount is not None
privkey, locktime = privkey_locktime
privkey = hexlify(privkey).decode()
pubkey = btc.privkey_to_pubkey(privkey)
pubkey = unhexlify(pubkey)
redeem_script = cls.pubkey_to_script_code((pubkey, locktime))
tx = btc.serialize(tx)
sig = btc.get_p2sh_signature(tx, index, redeem_script, privkey,
amount)
return btc.apply_freeze_signature(tx, index, redeem_script, sig)
class BTC_Watchonly_Timelocked_P2WSH(BTC_Timelocked_P2WSH):
@classmethod
def get_watchonly_path(cls, path):
#given path is something like "m/49'/1'/0'/0/0"
#but watchonly wallet already stores the xpub for "m/49'/1'/0'/"
#so to make this work we must chop off the first 3 elements
return path[3:]
@classmethod
def derive_bip32_privkey(cls, master_key, path):
assert len(path) > 1
return cls._walk_bip32_path(master_key, cls.get_watchonly_path(
path))[-1]
@classmethod
def key_to_script(cls, pubkey_locktime):
pub, locktime = pubkey_locktime
return cls.pubkey_to_script((pub, locktime))
@classmethod
def privkey_to_wif(cls, privkey_locktime):
return ""
@classmethod
def sign_transaction(cls, tx, index, privkey, amount,
hashcode=btc.SIGHASH_ALL, **kwargs):
raise RuntimeError("Cannot spend from watch-only wallets")
class BTC_Watchonly_P2SH_P2WPKH(BTC_P2SH_P2WPKH):
@classmethod
def derive_bip32_privkey(cls, master_key, path):
return BTC_Watchonly_Timelocked_P2WSH.derive_bip32_privkey(master_key, path)
@classmethod
def privkey_to_wif(cls, privkey_locktime):
return BTC_Watchonly_Timelocked_P2WSH.privkey_to_wif(privkey_locktime)
@staticmethod
def privkey_to_pubkey(privkey):
#in watchonly wallets there are no privkeys, so functions
# like _get_key_from_path() actually return pubkeys and
# this function is a noop
return privkey
@classmethod
def derive_bip32_pub_export(cls, master_key, path):
return super(BTC_Watchonly_P2SH_P2WPKH, cls).derive_bip32_pub_export(
master_key, BTC_Watchonly_Timelocked_P2WSH.get_watchonly_path(path))
@classmethod
def sign_transaction(cls, tx, index, privkey, amount,
hashcode=btc.SIGHASH_ALL, **kwargs):
raise RuntimeError("Cannot spend from watch-only wallets")
ENGINES = {
TYPE_P2PKH: BTC_P2PKH,
TYPE_P2SH_P2WPKH: BTC_P2SH_P2WPKH,
TYPE_P2WPKH: BTC_P2WPKH
}
TYPE_P2WPKH: BTC_P2WPKH,
TYPE_TIMELOCK_P2WSH: BTC_Timelocked_P2WSH,
TYPE_WATCHONLY_TIMELOCK_P2WSH: BTC_Watchonly_Timelocked_P2WSH,
TYPE_WATCHONLY_P2SH_P2WPKH: BTC_Watchonly_P2SH_P2WPKH
}

84
jmclient/jmclient/taker_utils.py

@ -5,13 +5,16 @@ import os
import sys
import time
import numbers
from binascii import hexlify
from jmbase import get_log, jmprint
from .configure import jm_single, validate_address
from .configure import jm_single, validate_address, is_burn_destination
from .schedule import human_readable_schedule_entry, tweak_tumble_schedule,\
schedule_to_text
from .wallet import BaseWallet, estimate_tx_fee, compute_tx_locktime
from .wallet import BaseWallet, estimate_tx_fee, compute_tx_locktime, \
FidelityBondMixin
from jmbitcoin import deserialize, make_shuffled_tx, serialize, txhash,\
amount_to_str
amount_to_str, mk_burn_script, bin_hash160
from jmbase.support import EXIT_SUCCESS
log = get_log()
@ -20,7 +23,7 @@ Utility functions for tumbler-style takers;
Currently re-used by CLI script tumbler.py and joinmarket-qt
"""
def direct_send(wallet_service, amount, mixdepth, destaddr, answeryes=False,
def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False,
accept_callback=None, info_callback=None):
"""Send coins directly from one mixdepth to one destination address;
does not need IRC. Sweep as for normal sendpayment (set amount=0).
@ -41,24 +44,65 @@ def direct_send(wallet_service, amount, mixdepth, destaddr, answeryes=False,
The txid if transaction is pushed, False otherwise
"""
#Sanity checks
assert validate_address(destaddr)[0]
assert validate_address(destination)[0] or is_burn_destination(destination)
assert isinstance(mixdepth, numbers.Integral)
assert mixdepth >= 0
assert isinstance(amount, numbers.Integral)
assert amount >=0
assert isinstance(wallet_service.wallet, BaseWallet)
if is_burn_destination(destination):
#Additional checks
if not isinstance(wallet_service.wallet, FidelityBondMixin):
log.error("Only fidelity bond wallets can burn coins")
return
if answeryes:
log.error("Burning coins not allowed without asking for confirmation")
return
if mixdepth != FidelityBondMixin.FIDELITY_BOND_MIXDEPTH:
log.error("Burning coins only allowed from mixdepth " + str(
FidelityBondMixin.FIDELITY_BOND_MIXDEPTH))
return
if amount != 0:
log.error("Only sweeping allowed when burning coins, to keep the tx " +
"small. Tip: use the coin control feature to freeze utxos")
return
from pprint import pformat
txtype = wallet_service.get_txtype()
if amount == 0:
utxos = wallet_service.get_utxos_by_mixdepth()[mixdepth]
if utxos == {}:
log.error(
"There are no utxos in mixdepth: " + str(mixdepth) + ", quitting.")
"There are no available utxos in mixdepth: " + str(mixdepth) + ", quitting.")
return
total_inputs_val = sum([va['value'] for u, va in iteritems(utxos)])
fee_est = estimate_tx_fee(len(utxos), 1, txtype=txtype)
outs = [{"address": destaddr, "value": total_inputs_val - fee_est}]
if is_burn_destination(destination):
if len(utxos) > 1:
log.error("Only one input allowed when burning coins, to keep "
+ "the tx small. Tip: use the coin control feature to freeze utxos")
return
address_type = FidelityBondMixin.BIP32_BURN_ID
index = wallet_service.wallet.get_next_unused_index(mixdepth, address_type)
path = wallet_service.wallet.get_path(mixdepth, address_type, index)
privkey, engine = wallet_service.wallet._get_key_from_path(path)
pubkey = engine.privkey_to_pubkey(privkey)
pubkeyhash = bin_hash160(pubkey)
#size of burn output is slightly different from regular outputs
burn_script = mk_burn_script(pubkeyhash) #in hex
fee_est = estimate_tx_fee(len(utxos), 0, txtype=txtype, extra_bytes=len(burn_script)/2)
outs = [{"script": burn_script, "value": total_inputs_val - fee_est}]
destination = "BURNER OUTPUT embedding pubkey at " \
+ wallet_service.wallet.get_path_repr(path) \
+ "\n\nWARNING: This transaction if broadcasted will PERMANENTLY DESTROY your bitcoins\n"
else:
#regular send (non-burn)
fee_est = estimate_tx_fee(len(utxos), 1, txtype=txtype)
outs = [{"address": destination, "value": total_inputs_val - fee_est}]
else:
#8 inputs to be conservative
initial_fee_est = estimate_tx_fee(8,2, txtype=txtype)
@ -69,30 +113,46 @@ def direct_send(wallet_service, amount, mixdepth, destaddr, answeryes=False,
fee_est = initial_fee_est
total_inputs_val = sum([va['value'] for u, va in iteritems(utxos)])
changeval = total_inputs_val - fee_est - amount
outs = [{"value": amount, "address": destaddr}]
outs = [{"value": amount, "address": destination}]
change_addr = wallet_service.get_internal_addr(mixdepth)
outs.append({"value": changeval, "address": change_addr})
#compute transaction locktime, has special case for spending timelocked coins
tx_locktime = compute_tx_locktime()
if mixdepth == FidelityBondMixin.FIDELITY_BOND_MIXDEPTH and \
isinstance(wallet_service.wallet, FidelityBondMixin):
for outpoint, utxo in utxos.items():
path = wallet_service.script_to_path(
wallet_service.addr_to_script(utxo["address"]))
if not FidelityBondMixin.is_timelocked_path(path):
continue
path_locktime = path[-1]
tx_locktime = max(tx_locktime, path_locktime+1)
#compute_tx_locktime() gives a locktime in terms of block height
#timelocked addresses use unix time instead
#OP_CHECKLOCKTIMEVERIFY can only compare like with like, so we
#must use unix time as the transaction locktime
#Now ready to construct transaction
log.info("Using a fee of : " + amount_to_str(fee_est) + ".")
if amount != 0:
log.info("Using a change value of: " + amount_to_str(changeval) + ".")
txsigned = sign_tx(wallet_service, make_shuffled_tx(
list(utxos.keys()), outs, False, 2, compute_tx_locktime()), utxos)
list(utxos.keys()), outs, False, 2, tx_locktime), utxos)
log.info("Got signed transaction:\n")
log.info(pformat(txsigned))
tx = serialize(txsigned)
log.info("In serialized form (for copy-paste):")
log.info(tx)
actual_amount = amount if amount != 0 else total_inputs_val - fee_est
log.info("Sends: " + amount_to_str(actual_amount) + " to address: " + destaddr)
log.info("Sends: " + amount_to_str(actual_amount) + " to destination: " + destination)
if not answeryes:
if not accept_callback:
if input('Would you like to push to the network? (y/n):')[0] != 'y':
log.info("You chose not to broadcast the transaction, quitting.")
return False
else:
accepted = accept_callback(pformat(txsigned), destaddr, actual_amount,
accepted = accept_callback(pformat(txsigned), destination, actual_amount,
fee_est)
if not accepted:
return False

505
jmclient/jmclient/wallet.py

@ -7,6 +7,7 @@ import numbers
import random
from binascii import hexlify, unhexlify
from datetime import datetime
from calendar import timegm
from copy import deepcopy
from mnemonic import Mnemonic as MnemonicParent
from hashlib import sha256
@ -20,7 +21,9 @@ from .blockchaininterface import INF_HEIGHT
from .support import select_gradual, select_greedy, select_greediest, \
select
from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH,\
TYPE_P2WPKH, ENGINES
TYPE_P2WPKH, TYPE_TIMELOCK_P2WSH, TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS,\
TYPE_WATCHONLY_FIDELITY_BONDS, TYPE_WATCHONLY_TIMELOCK_P2WSH, TYPE_WATCHONLY_P2SH_P2WPKH,\
ENGINES
from .support import get_random_bytes
from . import mn_encode, mn_decode
import jmbitcoin as btc
@ -66,7 +69,7 @@ class Mnemonic(MnemonicParent):
def detect_language(cls, code):
return "english"
def estimate_tx_fee(ins, outs, txtype='p2pkh'):
def estimate_tx_fee(ins, outs, txtype='p2pkh', extra_bytes=0):
'''Returns an estimate of the number of satoshis required
for a transaction with the given number of inputs and outputs,
based on information from the blockchain interface.
@ -79,13 +82,14 @@ def estimate_tx_fee(ins, outs, txtype='p2pkh'):
raise ValueError("Estimated fee per kB greater than absurd value: " + \
str(absurd_fee) + ", quitting.")
if txtype in ['p2pkh', 'p2shMofN']:
tx_estimated_bytes = btc.estimate_tx_size(ins, outs, txtype)
tx_estimated_bytes = btc.estimate_tx_size(ins, outs, txtype) + extra_bytes
return int((tx_estimated_bytes * fee_per_kb)/Decimal(1000.0))
elif txtype in ['p2wpkh', 'p2sh-p2wpkh']:
witness_estimate, non_witness_estimate = btc.estimate_tx_size(
ins, outs, txtype)
non_witness_estimate += extra_bytes
return int(int((
non_witness_estimate + 0.25*witness_estimate)*fee_per_kb)/Decimal(1000.0))
non_witness_estimate + 0.25*witness_estimate)*fee_per_kb)/Decimal(1000.0))
else:
raise NotImplementedError("Txtype: " + txtype + " not implemented.")
@ -299,6 +303,9 @@ class BaseWallet(object):
_ENGINE = None
ADDRESS_TYPE_EXTERNAL = 0
ADDRESS_TYPE_INTERNAL = 1
def __init__(self, storage, gap_limit=6, merge_algorithm_name=None,
mixdepth=None):
# to be defined by inheriting classes
@ -411,7 +418,8 @@ class BaseWallet(object):
"""
if self.TYPE == TYPE_P2PKH:
return 'p2pkh'
elif self.TYPE == TYPE_P2SH_P2WPKH:
elif self.TYPE in (TYPE_P2SH_P2WPKH,
TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS):
return 'p2sh-p2wpkh'
elif self.TYPE == TYPE_P2WPKH:
return 'p2wpkh'
@ -431,7 +439,7 @@ class BaseWallet(object):
for index, (script, amount) in scripts.items():
assert amount > 0
path = self.script_to_path(script)
privkey, engine = self._get_priv_from_path(path)
privkey, engine = self._get_key_from_path(path)
tx = btc.deserialize(engine.sign_transaction(tx, index, privkey,
amount, **kwargs))
return tx
@ -443,12 +451,16 @@ class BaseWallet(object):
"""
script = self._ENGINE.address_to_script(addr)
path = self.script_to_path(script)
privkey = self._get_priv_from_path(path)[0]
privkey = self._get_key_from_path(path)[0]
return hexlify(privkey).decode('ascii')
def _get_addr_int_ext(self, internal, mixdepth):
script = self.get_internal_script(mixdepth) if internal else \
self.get_external_script(mixdepth)
def _get_addr_int_ext(self, address_type, mixdepth):
if address_type == self.ADDRESS_TYPE_EXTERNAL:
script = self.get_external_script(mixdepth)
elif address_type == self.ADDRESS_TYPE_INTERNAL:
script = self.get_internal_script(mixdepth)
else:
assert 0
return self.script_to_addr(script)
def get_external_addr(self, mixdepth):
@ -457,20 +469,20 @@ class BaseWallet(object):
the wallet from other sources, or receiving payments or donations.
JoinMarket will never generate these addresses for internal use.
"""
return self._get_addr_int_ext(False, mixdepth)
return self._get_addr_int_ext(self.ADDRESS_TYPE_EXTERNAL, mixdepth)
def get_internal_addr(self, mixdepth):
"""
Return an address for internal usage, as change addresses and when
participating in transactions initiated by other parties.
"""
return self._get_addr_int_ext(True, mixdepth)
return self._get_addr_int_ext(self.ADDRESS_TYPE_INTERNAL, mixdepth)
def get_external_script(self, mixdepth):
return self.get_new_script(mixdepth, False)
return self.get_new_script(mixdepth, self.ADDRESS_TYPE_EXTERNAL)
def get_internal_script(self, mixdepth):
return self.get_new_script(mixdepth, True)
return self.get_new_script(mixdepth, self.ADDRESS_TYPE_INTERNAL)
@classmethod
def addr_to_script(cls, addr):
@ -487,7 +499,7 @@ class BaseWallet(object):
def script_to_addr(self, script):
assert self.is_known_script(script)
path = self.script_to_path(script)
engine = self._get_priv_from_path(path)[1]
engine = self._get_key_from_path(path)[1]
return engine.script_to_address(script)
def get_script_code(self, script):
@ -499,7 +511,7 @@ class BaseWallet(object):
For non-segwit wallets, raises EngineError.
"""
path = self.script_to_path(script)
priv, engine = self._get_priv_from_path(path)
priv, engine = self._get_key_from_path(path)
pub = engine.privkey_to_pubkey(priv)
return engine.pubkey_to_script_code(pub)
@ -512,47 +524,47 @@ class BaseWallet(object):
return cls._ENGINE.pubkey_has_script(pubkey, script)
@deprecated
def get_key(self, mixdepth, internal, index):
def get_key(self, mixdepth, address_type, index):
raise NotImplementedError()
def get_addr(self, mixdepth, internal, index):
script = self.get_script(mixdepth, internal, index)
def get_addr(self, mixdepth, address_type, index):
script = self.get_script(mixdepth, address_type, index)
return self.script_to_addr(script)
def get_address_from_path(self, path):
script = self.get_script_from_path(path)
return self.script_to_addr(script)
def get_new_addr(self, mixdepth, internal):
def get_new_addr(self, mixdepth, address_type):
"""
use get_external_addr/get_internal_addr
"""
script = self.get_new_script(mixdepth, internal)
script = self.get_new_script(mixdepth, address_type)
return self.script_to_addr(script)
def get_new_script(self, mixdepth, internal):
def get_new_script(self, mixdepth, address_type):
raise NotImplementedError()
def get_wif(self, mixdepth, internal, index):
return self.get_wif_path(self.get_path(mixdepth, internal, index))
def get_wif(self, mixdepth, address_type, index):
return self.get_wif_path(self.get_path(mixdepth, address_type, index))
def get_wif_path(self, path):
priv, engine = self._get_priv_from_path(path)
priv, engine = self._get_key_from_path(path)
return engine.privkey_to_wif(priv)
def get_path(self, mixdepth=None, internal=None, index=None):
def get_path(self, mixdepth=None, address_type=None, index=None):
raise NotImplementedError()
def get_details(self, path):
"""
Return mixdepth, internal, index for a given path
Return mixdepth, address_type, index for a given path
args:
path: wallet path
returns:
tuple (mixdepth, type, index)
type is one of 0, 1, 'imported'
type is one of 0, 1, 'imported', 2, 3
"""
raise NotImplementedError()
@ -814,11 +826,11 @@ class BaseWallet(object):
"""
raise NotImplementedError()
def get_script(self, mixdepth, internal, index):
path = self.get_path(mixdepth, internal, index)
def get_script(self, mixdepth, address_type, index):
path = self.get_path(mixdepth, address_type, index)
return self.get_script_from_path(path)
def _get_priv_from_path(self, path):
def _get_key_from_path(self, path):
raise NotImplementedError()
def get_path_repr(self, path):
@ -843,7 +855,7 @@ class BaseWallet(object):
"""
raise NotImplementedError()
def get_next_unused_index(self, mixdepth, internal):
def get_next_unused_index(self, mixdepth, address_type):
"""
Get the next index for public scripts/addresses not yet handed out.
@ -873,7 +885,7 @@ class BaseWallet(object):
returns:
signature as base64-encoded string
"""
priv, engine = self._get_priv_from_path(path)
priv, engine = self._get_key_from_path(path)
return engine.sign_message(priv, message)
def get_wallet_name(self):
@ -952,13 +964,13 @@ class BaseWallet(object):
assert script in self._script_map
return self._script_map[script]
def set_next_index(self, mixdepth, internal, index, force=False):
def set_next_index(self, mixdepth, address_type, index, force=False):
"""
Set the next index to use when generating a new key pair.
params:
mixdepth: int
internal: 0/False or 1/True
address_type: 0 (external) or 1 (internal)
index: int
force: True if you know the wallet already knows all scripts
up to (excluding) the given index
@ -969,28 +981,31 @@ class BaseWallet(object):
def rewind_wallet_indices(self, used_indices, saved_indices):
for md in used_indices:
for int_type in (0, 1):
index = max(used_indices[md][int_type],
saved_indices[md][int_type])
self.set_next_index(md, int_type, index, force=True)
for address_type in range(min(len(used_indices[md]), len(saved_indices[md]))):
index = max(used_indices[md][address_type],
saved_indices[md][address_type])
self.set_next_index(md, address_type, index, force=True)
def _get_default_used_indices(self):
return {x: [0, 0] for x in range(self.max_mixdepth + 1)}
def get_used_indices(self, addr_gen):
""" Returns a dict of max used indices for each branch in
the wallet, from the given addresses addr_gen, assuming
that they are known to the wallet.
"""
indices = {x: [0, 0] for x in range(self.max_mixdepth + 1)}
indices = self._get_default_used_indices()
for addr in addr_gen:
if not self.is_known_addr(addr):
continue
md, internal, index = self.get_details(
md, address_type, index = self.get_details(
self.addr_to_path(addr))
if internal not in (0, 1):
assert internal == 'imported'
if address_type not in (self.BIP32_EXT_ID, self.BIP32_INT_ID,
FidelityBondMixin.BIP32_TIMELOCK_ID, FidelityBondMixin.BIP32_BURN_ID):
assert address_type == 'imported'
continue
indices[md][internal] = max(indices[md][internal], index + 1)
indices[md][address_type] = max(indices[md][address_type], index + 1)
return indices
@ -1001,9 +1016,10 @@ class BaseWallet(object):
cache."""
for md in used_indices:
for internal in (0, 1):
if used_indices[md][internal] >\
max(self.get_next_unused_index(md, internal), 0):
for address_type in (self.ADDRESS_TYPE_EXTERNAL,
self.ADDRESS_TYPE_INTERNAL):
if used_indices[md][address_type] >\
max(self.get_next_unused_index(md, address_type), 0):
return False
return True
@ -1094,7 +1110,7 @@ class ImportWalletMixin(object):
#key_type = key_type_wif if key_type_wif is not None else self.TYPE
engine = self._ENGINES[key_type]
if engine.privkey_to_script(privkey) in self._script_map:
if engine.key_to_script(privkey) in self._script_map:
raise WalletError("Cannot import key, already in wallet: {}"
"".format(wif))
@ -1139,7 +1155,7 @@ class ImportWalletMixin(object):
engine = self._ENGINES[key_type]
path = (self._IMPORTED_ROOT_PATH, mixdepth, index)
self._script_map[engine.privkey_to_script(privkey)] = path
self._script_map[engine.key_to_script(privkey)] = path
return path
@ -1150,9 +1166,9 @@ class ImportWalletMixin(object):
assert len(path) == 3
return path[1]
def _get_priv_from_path(self, path):
def _get_key_from_path(self, path):
if not self._is_imported_path(path):
return super(ImportWalletMixin, self)._get_priv_from_path(path)
return super(ImportWalletMixin, self)._get_key_from_path(path)
assert len(path) == 3
md, i = path[1], path[2]
@ -1204,8 +1220,8 @@ class ImportWalletMixin(object):
if not self._is_imported_path(path):
return super(ImportWalletMixin, self).get_script_from_path(path)
priv, engine = self._get_priv_from_path(path)
return engine.privkey_to_script(priv)
priv, engine = self._get_key_from_path(path)
return engine.key_to_script(priv)
class BIP39WalletMixin(object):
@ -1265,13 +1281,14 @@ class BIP32Wallet(BaseWallet):
_STORAGE_ENTROPY_KEY = b'entropy'
_STORAGE_INDEX_CACHE = b'index_cache'
BIP32_MAX_PATH_LEVEL = 2**31
BIP32_EXT_ID = 0
BIP32_INT_ID = 1
BIP32_EXT_ID = BaseWallet.ADDRESS_TYPE_EXTERNAL
BIP32_INT_ID = BaseWallet.ADDRESS_TYPE_INTERNAL
ENTROPY_BYTES = 16
def __init__(self, storage, **kwargs):
self._entropy = None
# {mixdepth: {type: index}} with type being 0/1 for [non]-internal
# {mixdepth: {type: index}} with type being 0/1 corresponding
# to external/internal addresses
self._index_cache = None
# path is a tuple of BIP32 levels,
# m is the master key's fingerprint
@ -1287,9 +1304,7 @@ class BIP32Wallet(BaseWallet):
# used to verify paths for sanity checking and for wallet id creation
self._key_ident = b'' # otherwise get_bip32_* won't work
self._key_ident = sha256(sha256(
self.get_bip32_priv_export(0, 0).encode('ascii')).digest())\
.digest()[:3]
self._key_ident = self._get_key_ident()
self._populate_script_map()
self.disable_new_scripts = False
@ -1331,11 +1346,16 @@ class BIP32Wallet(BaseWallet):
self.max_mixdepth = max(0, 0, *self._index_cache.keys())
def _get_key_ident(self):
return sha256(sha256(
self.get_bip32_priv_export(0, 0).encode('ascii')).digest())\
.digest()[:3]
def _populate_script_map(self):
for md in self._index_cache:
for int_type in (self.BIP32_EXT_ID, self.BIP32_INT_ID):
for i in range(self._index_cache[md][int_type]):
path = self.get_path(md, int_type, i)
for address_type in (self.BIP32_EXT_ID, self.BIP32_INT_ID):
for i in range(self._index_cache[md][address_type]):
path = self.get_path(md, address_type, i)
script = self.get_script_from_path(path)
self._script_map[script] = path
@ -1368,47 +1388,53 @@ class BIP32Wallet(BaseWallet):
def _derive_bip32_master_key(cls, seed):
return cls._ENGINE.derive_bip32_master_key(seed)
@classmethod
def _get_supported_address_types(cls):
return (cls.BIP32_EXT_ID, cls.BIP32_INT_ID)
def get_script_from_path(self, path):
if not self._is_my_bip32_path(path):
raise WalletError("unable to get script for unknown key path")
md, int_type, index = self.get_details(path)
md, address_type, index = self.get_details(path)
if not 0 <= md <= self.max_mixdepth:
raise WalletError("Mixdepth outside of wallet's range.")
assert int_type in (self.BIP32_EXT_ID, self.BIP32_INT_ID)
assert address_type in self._get_supported_address_types()
current_index = self._index_cache[md][int_type]
current_index = self._index_cache[md][address_type]
if index == current_index:
return self.get_new_script_override_disable(md, int_type)
if index == current_index \
and address_type != FidelityBondMixin.BIP32_TIMELOCK_ID:
#special case for timelocked addresses because for them the
#concept of a "next address" cant be used
return self.get_new_script_override_disable(md, address_type)
priv, engine = self._get_priv_from_path(path)
script = engine.privkey_to_script(priv)
priv, engine = self._get_key_from_path(path)
script = engine.key_to_script(priv)
return script
def get_path(self, mixdepth=None, internal=None, index=None):
def get_path(self, mixdepth=None, address_type=None, index=None):
if mixdepth is not None:
assert isinstance(mixdepth, Integral)
if not 0 <= mixdepth <= self.max_mixdepth:
raise WalletError("Mixdepth outside of wallet's range.")
if internal is not None:
if address_type is not None:
if mixdepth is None:
raise Exception("mixdepth must be set if internal is set")
int_type = self._get_internal_type(internal)
raise Exception("mixdepth must be set if address_type is set")
if index is not None:
assert isinstance(index, Integral)
if internal is None:
raise Exception("internal must be set if index is set")
assert index <= self._index_cache[mixdepth][int_type]
if address_type is None:
raise Exception("address_type must be set if index is set")
assert index <= self._index_cache[mixdepth][address_type]
assert index < self.BIP32_MAX_PATH_LEVEL
return tuple(chain(self._get_bip32_export_path(mixdepth, internal),
return tuple(chain(self._get_bip32_export_path(mixdepth, address_type),
(index,)))
return tuple(self._get_bip32_export_path(mixdepth, internal))
return tuple(self._get_bip32_export_path(mixdepth, address_type))
def get_path_repr(self, path):
path = list(path)
@ -1452,7 +1478,7 @@ class BIP32Wallet(BaseWallet):
return path[len(self._get_bip32_base_path())]
def _get_priv_from_path(self, path):
def _get_key_from_path(self, path):
if not self._is_my_bip32_path(path):
raise WalletError("Invalid path, unknown root: {}".format(path))
@ -1462,53 +1488,57 @@ class BIP32Wallet(BaseWallet):
def _is_my_bip32_path(self, path):
return path[0] == self._key_ident
def get_new_script(self, mixdepth, internal):
def get_new_script(self, mixdepth, address_type):
if self.disable_new_scripts:
raise RuntimeError("Obtaining new wallet addresses "
+ "disabled, due to nohistory mode")
return self.get_new_script_override_disable(mixdepth, internal)
return self.get_new_script_override_disable(mixdepth, address_type)
def get_new_script_override_disable(self, mixdepth, internal):
def get_new_script_override_disable(self, mixdepth, address_type):
# This is called by get_script_from_path and calls back there. We need to
# ensure all conditions match to avoid endless recursion.
int_type = self._get_internal_type(internal)
index = self._index_cache[mixdepth][int_type]
self._index_cache[mixdepth][int_type] += 1
path = self.get_path(mixdepth, int_type, index)
index = self.get_index_cache_and_increment(mixdepth, address_type)
return self.get_script_and_update_map(mixdepth, address_type, index)
def get_index_cache_and_increment(self, mixdepth, address_type):
index = self._index_cache[mixdepth][address_type]
self._index_cache[mixdepth][address_type] += 1
return index
def get_script_and_update_map(self, *args):
path = self.get_path(*args)
script = self.get_script_from_path(path)
self._script_map[script] = path
return script
def get_script(self, mixdepth, internal, index):
path = self.get_path(mixdepth, internal, index)
def get_script(self, mixdepth, address_type, index):
path = self.get_path(mixdepth, address_type, index)
return self.get_script_from_path(path)
@deprecated
def get_key(self, mixdepth, internal, index):
int_type = self._get_internal_type(internal)
path = self.get_path(mixdepth, int_type, index)
def get_key(self, mixdepth, address_type, index):
path = self.get_path(mixdepth, address_type, index)
priv = self._ENGINE.derive_bip32_privkey(self._master_key, path)
return hexlify(priv).decode('ascii')
def get_bip32_priv_export(self, mixdepth=None, internal=None):
path = self._get_bip32_export_path(mixdepth, internal)
def get_bip32_priv_export(self, mixdepth=None, address_type=None):
path = self._get_bip32_export_path(mixdepth, address_type)
return self._ENGINE.derive_bip32_priv_export(self._master_key, path)
def get_bip32_pub_export(self, mixdepth=None, internal=None):
path = self._get_bip32_export_path(mixdepth, internal)
def get_bip32_pub_export(self, mixdepth=None, address_type=None):
path = self._get_bip32_export_path(mixdepth, address_type)
return self._ENGINE.derive_bip32_pub_export(self._master_key, path)
def _get_bip32_export_path(self, mixdepth=None, internal=None):
def _get_bip32_export_path(self, mixdepth=None, address_type=None):
if mixdepth is None:
assert internal is None
assert address_type is None
path = tuple()
else:
assert 0 <= mixdepth <= self.max_mixdepth
if internal is None:
if address_type is None:
path = (self._get_bip32_mixdepth_path_level(mixdepth),)
else:
int_type = self._get_internal_type(internal)
path = (self._get_bip32_mixdepth_path_level(mixdepth), int_type)
path = (self._get_bip32_mixdepth_path_level(mixdepth), address_type)
return tuple(chain(self._get_bip32_base_path(), path))
@ -1519,19 +1549,15 @@ class BIP32Wallet(BaseWallet):
def _get_bip32_mixdepth_path_level(cls, mixdepth):
return mixdepth
def _get_internal_type(self, is_internal):
return self.BIP32_INT_ID if is_internal else self.BIP32_EXT_ID
def get_next_unused_index(self, mixdepth, internal):
def get_next_unused_index(self, mixdepth, address_type):
assert 0 <= mixdepth <= self.max_mixdepth
int_type = self._get_internal_type(internal)
if self._index_cache[mixdepth][int_type] >= self.BIP32_MAX_PATH_LEVEL:
if self._index_cache[mixdepth][address_type] >= self.BIP32_MAX_PATH_LEVEL:
# FIXME: theoretically this should work for up to
# self.BIP32_MAX_PATH_LEVEL * 2, no?
raise WalletError("All addresses used up, cannot generate new ones.")
return self._index_cache[mixdepth][int_type]
return self._index_cache[mixdepth][address_type]
def get_mnemonic_words(self):
return ' '.join(mn_encode(hexlify(self._entropy).decode('ascii'))), None
@ -1547,11 +1573,10 @@ class BIP32Wallet(BaseWallet):
def get_wallet_id(self):
return hexlify(self._key_ident).decode('ascii')
def set_next_index(self, mixdepth, internal, index, force=False):
int_type = self._get_internal_type(internal)
if not (force or index <= self._index_cache[mixdepth][int_type]):
def set_next_index(self, mixdepth, address_type, index, force=False):
if not (force or index <= self._index_cache[mixdepth][address_type]):
raise Exception("cannot advance index without force=True")
self._index_cache[mixdepth][int_type] = index
self._index_cache[mixdepth][address_type] = index
def get_details(self, path):
if not self._is_my_bip32_path(path):
@ -1569,8 +1594,6 @@ class LegacyWallet(ImportWalletMixin, BIP32Wallet):
def _get_bip32_base_path(self):
return self._key_ident, 0
class BIP32PurposedWallet(BIP32Wallet):
""" A class to encapsulate cases like
BIP44, 49 and 84, all of which are derivatives
@ -1593,6 +1616,233 @@ class BIP32PurposedWallet(BIP32Wallet):
return path[len(self._get_bip32_base_path())] - 2**31
class FidelityBondMixin(object):
BIP32_TIMELOCK_ID = 2
BIP32_BURN_ID = 3
"""
Explaination of time number
incrementing time numbers (0, 1, 2, 3, 4...
will produce datetimes
suitable for timelocking (1st january, 1st april, 1st july ....
this greatly reduces the number of possible timelock values
and is helpful for recovery of funds because the wallet can search
the only addresses corresponding to timenumbers which are far fewer
For example, if TIMENUMBER_UNIT = 2 (i.e. every time number is two months)
then there are 6 timelocks per year so just 600 possible
addresses per century per pubkey. Easily searchable when recovering a
wallet from seed phrase. Therefore the user doesn't need to store any
dates, the seed phrase is sufficent for recovery.
"""
#should be a factor of 12, the number of months in a year
TIMENUMBER_UNIT = 1
# all timelocks are 1st of the month at midnight
TIMELOCK_DAY_AND_SHORTER = (1, 0, 0, 0, 0)
TIMELOCK_EPOCH_YEAR = 2020
TIMELOCK_EPOCH_MONTH = 1 #january
MONTHS_IN_YEAR = 12
TIMELOCK_ERA_YEARS = 30
TIMENUMBERS_PER_PUBKEY = TIMELOCK_ERA_YEARS * MONTHS_IN_YEAR // TIMENUMBER_UNIT
"""
As each pubkey corresponds to hundreds of addresses, to reduce load the
given gap limit will be reduced by this factor. Also these timelocked
addresses are never handed out to takers so there wont be a problem of
having many used addresses with no transactions on them.
"""
TIMELOCK_GAP_LIMIT_REDUCTION_FACTOR = 6
_TIMELOCK_ENGINE = ENGINES[TYPE_TIMELOCK_P2WSH]
#only one mixdepth will have fidelity bonds in it
FIDELITY_BOND_MIXDEPTH = 0
MERKLE_BRANCH_UNAVAILABLE = b"mbu"
_BURNER_OUTPUT_STORAGE_KEY = b"burner-out"
_BIP32_PUBKEY_PREFIX = "fbonds-mpk-"
@classmethod
def _time_number_to_timestamp(cls, timenumber):
"""
converts a time number to a unix timestamp
"""
if not 0 <= timenumber < cls.TIMENUMBERS_PER_PUBKEY:
raise ValueError()
year = cls.TIMELOCK_EPOCH_YEAR + (timenumber*cls.TIMENUMBER_UNIT) // cls.MONTHS_IN_YEAR
month = cls.TIMELOCK_EPOCH_MONTH + (timenumber*cls.TIMENUMBER_UNIT) % cls.MONTHS_IN_YEAR
return timegm(datetime(year, month, *cls.TIMELOCK_DAY_AND_SHORTER).timetuple())
@classmethod
def timestamp_to_time_number(cls, timestamp):
"""
converts a datetime object to a time number
"""
dt = datetime.utcfromtimestamp(timestamp)
if (dt.month - cls.TIMELOCK_EPOCH_MONTH) % cls.TIMENUMBER_UNIT != 0:
raise ValueError()
day_and_shorter_tuple = (dt.day, dt.hour, dt.minute, dt.second, dt.microsecond)
if day_and_shorter_tuple != cls.TIMELOCK_DAY_AND_SHORTER:
raise ValueError()
timenumber = (dt.year - cls.TIMELOCK_EPOCH_YEAR)*(cls.MONTHS_IN_YEAR //
cls.TIMENUMBER_UNIT) + ((dt.month - cls.TIMELOCK_EPOCH_MONTH) // cls.TIMENUMBER_UNIT)
if timenumber < 0 or timenumber > cls.TIMENUMBERS_PER_PUBKEY:
raise ValueError("datetime out of range")
return timenumber
@classmethod
def is_timelocked_path(cls, path):
return len(path) > 4 and path[4] == cls.BIP32_TIMELOCK_ID
def get_xpub_from_fidelity_bond_master_pub_key(cls, mpk):
if mpk.startswith(cls._BIP32_PUBKEY_PREFIX):
return mpk[len(cls._BIP32_PUBKEY_PREFIX):]
else:
return False
def _get_key_ident(self):
first_path = self.get_path(0, 0)
priv, engine = self._get_key_from_path(first_path)
pub = engine.privkey_to_pubkey(priv)
return sha256(sha256(pub).digest()).digest()[:3]
@classmethod
def get_xpub_from_fidelity_bond_master_pub_key(cls, mpk):
if mpk.startswith(cls._BIP32_PUBKEY_PREFIX):
return mpk[len(cls._BIP32_PUBKEY_PREFIX):]
else:
return False
def _populate_script_map(self):
super(FidelityBondMixin, self)._populate_script_map()
for md in self._index_cache:
address_type = self.BIP32_TIMELOCK_ID
for i in range(self._index_cache[md][address_type]):
for timenumber in range(self.TIMENUMBERS_PER_PUBKEY):
path = self.get_path(md, address_type, i, timenumber)
script = self.get_script_from_path(path)
self._script_map[script] = path
def add_utxo(self, txid, index, script, value, height=None):
super(FidelityBondMixin, self).add_utxo(txid, index, script, value,
height)
#dont use coin control freeze if wallet readonly
if self._storage.read_only:
return
path = self.script_to_path(script)
if not self.is_timelocked_path(path):
return
if datetime.utcfromtimestamp(path[-1]) > datetime.now():
#freeze utxo if its timelock is in the future
self.disable_utxo(txid, index, disable=True)
def get_bip32_pub_export(self, mixdepth=None, address_type=None):
bip32_pub = super(FidelityBondMixin, self).get_bip32_pub_export(mixdepth, address_type)
if address_type == None and mixdepth == self.FIDELITY_BOND_MIXDEPTH:
bip32_pub = self._BIP32_PUBKEY_PREFIX + bip32_pub
return bip32_pub
@classmethod
def _get_supported_address_types(cls):
return (cls.BIP32_EXT_ID, cls.BIP32_INT_ID, cls.BIP32_TIMELOCK_ID, cls.BIP32_BURN_ID)
def _get_key_from_path(self, path):
if self.is_timelocked_path(path):
key_path = path[:-1]
locktime = path[-1]
engine = self._TIMELOCK_ENGINE
privkey = engine.derive_bip32_privkey(self._master_key, key_path)
return (privkey, locktime), engine
else:
return super(FidelityBondMixin, self)._get_key_from_path(path)
def get_path(self, mixdepth=None, address_type=None, index=None, timenumber=None):
if address_type == None or address_type in (self.BIP32_EXT_ID, self.BIP32_INT_ID,
self.BIP32_BURN_ID) or index == None:
return super(FidelityBondMixin, self).get_path(mixdepth, address_type, index)
elif address_type == self.BIP32_TIMELOCK_ID:
assert timenumber != None
timestamp = self._time_number_to_timestamp(timenumber)
return tuple(chain(self._get_bip32_export_path(mixdepth, address_type),
(index, timestamp)))
else:
assert 0
"""
We define a new serialization of the bip32 path to include bip65 timelock addresses
Previously it was m/44'/1'/0'/3/0
For a timelocked address it will be m/44'/1'/0'/3/0:13245432
The timelock will be in unix format and added to the end with a colon ":" character
refering to the pubkey plus the timelock value which together are needed to create the address
"""
def get_path_repr(self, path):
if self.is_timelocked_path(path) and len(path) == 7:
return super(FidelityBondMixin, self).get_path_repr(path[:-1]) +\
":" + str(path[-1])
else:
return super(FidelityBondMixin, self).get_path_repr(path)
def path_repr_to_path(self, pathstr):
if pathstr.find(":") == -1:
return super(FidelityBondMixin, self).path_repr_to_path(pathstr)
else:
colon_chunks = pathstr.split(":")
if len(colon_chunks) != 2:
raise WalletError("Not a valid wallet timelock path: {}".format(pathstr))
return tuple(chain(
super(FidelityBondMixin, self).path_repr_to_path(colon_chunks[0]),
(int(colon_chunks[1]),)
))
def get_details(self, path):
if self.is_timelocked_path(path):
return self._get_mixdepth_from_path(path), path[-3], path[-2]
else:
return super(FidelityBondMixin, self).get_details(path)
def _get_default_used_indices(self):
return {x: [0, 0, 0, 0] for x in range(self.max_mixdepth + 1)}
def get_script(self, mixdepth, address_type, index, timenumber=None):
path = self.get_path(mixdepth, address_type, index, timenumber)
return self.get_script_from_path(path)
def get_addr(self, mixdepth, address_type, index, timenumber=None):
script = self.get_script(mixdepth, address_type, index, timenumber)
return self.script_to_addr(script)
def add_burner_output(self, path, txhex, block_height, merkle_branch,
block_index, write=True):
"""
merkle_branch = None means it was unavailable because of pruning
"""
if self._BURNER_OUTPUT_STORAGE_KEY not in self._storage.data:
self._storage.data[self._BURNER_OUTPUT_STORAGE_KEY] = {}
path = path.encode()
txhex = unhexlify(txhex)
if not merkle_branch:
merkle_branch = self.MERKLE_BRANCH_UNAVAILABLE
self._storage.data[self._BURNER_OUTPUT_STORAGE_KEY][path] = [txhex,
block_height, merkle_branch, block_index]
if write:
self._storage.save()
def get_burner_outputs(self):
"""
Result is a dict {path: [txhex, blockheight, merkleproof, blockindex]}
"""
return self._storage.data.get(self._BURNER_OUTPUT_STORAGE_KEY, {})
def set_burner_output_merkle_branch(self, path, merkle_branch):
path = path.encode()
self._storage.data[self._BURNER_OUTPUT_STORAGE_KEY][path][2] = \
merkle_branch
class BIP49Wallet(BIP32PurposedWallet):
_PURPOSE = 2**31 + 49
_ENGINE = ENGINES[TYPE_P2SH_P2WPKH]
@ -1607,8 +1857,33 @@ class SegwitLegacyWallet(ImportWalletMixin, BIP39WalletMixin, BIP49Wallet):
class SegwitWallet(ImportWalletMixin, BIP39WalletMixin, BIP84Wallet):
TYPE = TYPE_P2WPKH
class SegwitLegacyWalletFidelityBonds(FidelityBondMixin, SegwitLegacyWallet):
TYPE = TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS
class FidelityBondWatchonlyWallet(FidelityBondMixin, BIP49Wallet):
TYPE = TYPE_WATCHONLY_FIDELITY_BONDS
_ENGINE = ENGINES[TYPE_WATCHONLY_P2SH_P2WPKH]
_TIMELOCK_ENGINE = ENGINES[TYPE_WATCHONLY_TIMELOCK_P2WSH]
@classmethod
def _verify_entropy(cls, ent):
return ent[1:4] == b"pub"
@classmethod
def _derive_bip32_master_key(cls, master_entropy):
return btc.bip32_deserialize(master_entropy.decode())
def _get_bip32_export_path(self, mixdepth=None, address_type=None):
path = super(FidelityBondWatchonlyWallet, self)._get_bip32_export_path(
mixdepth, address_type)
return path
WALLET_IMPLEMENTATIONS = {
LegacyWallet.TYPE: LegacyWallet,
SegwitLegacyWallet.TYPE: SegwitLegacyWallet,
SegwitWallet.TYPE: SegwitWallet
SegwitWallet.TYPE: SegwitWallet,
SegwitLegacyWalletFidelityBonds.TYPE: SegwitLegacyWalletFidelityBonds,
FidelityBondWatchonlyWallet.TYPE: FidelityBondWatchonlyWallet
}

137
jmclient/jmclient/wallet_service.py

@ -5,6 +5,7 @@ import time
import ast
import binascii
import sys
import itertools
from decimal import Decimal
from copy import deepcopy
from twisted.internet import reactor
@ -15,7 +16,9 @@ from jmclient.configure import jm_single, get_log
from jmclient.output import fmt_tx_data
from jmclient.blockchaininterface import (INF_HEIGHT, BitcoinCoreInterface,
BitcoinCoreNoHistoryInterface)
from jmclient.wallet import FidelityBondMixin
from jmbase.support import jmprint, EXIT_SUCCESS
import jmbitcoin as btc
"""Wallet service
The purpose of this independent service is to allow
@ -516,6 +519,19 @@ class WalletService(Service):
# index:
self.bci.import_addresses(self.collect_addresses_gap(), self.get_wallet_name(),
self.restart_callback)
if isinstance(self.wallet, FidelityBondMixin):
mixdepth = FidelityBondMixin.FIDELITY_BOND_MIXDEPTH
address_type = FidelityBondMixin.BIP32_BURN_ID
burner_outputs = self.wallet.get_burner_outputs()
max_index = 0
for path_repr in burner_outputs:
index = self.wallet.path_repr_to_path(path_repr.decode())[-1]
max_index = max(index+1, max_index)
self.wallet.set_next_index(mixdepth, address_type, max_index,
force=True)
self.synced = True
def display_rescan_message_and_system_exit(self, restart_cb):
@ -536,6 +552,48 @@ class WalletService(Service):
jmprint(restart_msg, "important")
sys.exit(EXIT_SUCCESS)
def sync_burner_outputs(self, burner_txes):
mixdepth = FidelityBondMixin.FIDELITY_BOND_MIXDEPTH
address_type = FidelityBondMixin.BIP32_BURN_ID
self.wallet.set_next_index(mixdepth, address_type, self.wallet.gap_limit,
force=True)
highest_used_index = 0
known_burner_outputs = self.wallet.get_burner_outputs()
index = -1
while index - highest_used_index < self.wallet.gap_limit:
index += 1
self.wallet.set_next_index(mixdepth, address_type, index, force=True)
path = self.wallet.get_path(mixdepth, address_type, index)
path_privkey, engine = self.wallet._get_key_from_path(path)
path_pubkey = engine.privkey_to_pubkey(path_privkey)
path_pubkeyhash = btc.bin_hash160(path_pubkey)
for burner_tx in burner_txes:
burner_pubkeyhash, gettx = burner_tx
if burner_pubkeyhash != path_pubkeyhash:
continue
highest_used_index = index
path_repr = self.wallet.get_path_repr(path)
if path_repr.encode() in known_burner_outputs:
continue
txid = gettx["txid"]
jlog.info("Found a burner transaction txid=" + txid + " path = "
+ path_repr)
try:
merkle_branch = self.bci.get_tx_merkle_branch(txid, gettx["blockhash"])
except ValueError as e:
jlog.warning(repr(e))
jlog.warning("Merkle branch likely not available, use "
+ "wallet-tool `addtxoutproof`")
merkle_branch = None
block_height = self.bci.rpc("getblockheader", [gettx["blockhash"]])["height"]
if merkle_branch:
assert self.bci.verify_tx_merkle_branch(txid, block_height, merkle_branch)
self.wallet.add_burner_output(path_repr, gettx["hex"], block_height,
merkle_branch, gettx["blockindex"])
self.wallet.set_next_index(mixdepth, address_type, highest_used_index + 1)
def sync_addresses(self):
""" Triggered by use of --recoversync option in scripts,
attempts a full scan of the blockchain without assuming
@ -552,9 +610,35 @@ class WalletService(Service):
self.display_rescan_message_and_system_exit(self.restart_callback)
return
used_addresses_gen = (tx['address']
for tx in self.bci._yield_transactions(wallet_name)
if tx['category'] == 'receive')
if isinstance(self.wallet, FidelityBondMixin):
tx_receive = []
burner_txes = []
for tx in self.bci._yield_transactions(wallet_name):
if tx['category'] == 'receive':
tx_receive.append(tx)
elif tx["category"] == "send":
gettx = self.bci.get_transaction(tx["txid"])
txd = self.bci.get_deser_from_gettransaction(gettx)
if len(txd["outs"]) > 1:
continue
#must be mined into a block to sync
#otherwise there's no merkleproof or block index
if gettx["confirmations"] < 1:
continue
script = binascii.unhexlify(txd["outs"][0]["script"])
if script[0] != 0x6a: #OP_RETURN
continue
pubkeyhash = script[2:]
burner_txes.append((pubkeyhash, gettx))
self.sync_burner_outputs(burner_txes)
used_addresses_gen = (tx["address"] for tx in tx_receive)
else:
#not fidelity bond wallet, significantly faster sync
used_addresses_gen = (tx['address']
for tx in self.bci._yield_transactions(wallet_name)
if tx['category'] == 'receive')
used_indices = self.get_used_indices(used_addresses_gen)
jlog.debug("got used indices: {}".format(used_indices))
gap_limit_used = not self.check_gap_indices(used_indices)
@ -721,20 +805,36 @@ class WalletService(Service):
for md in range(self.max_mixdepth + 1):
saved_indices[md] = [0, 0]
for internal in (0, 1):
next_unused = self.get_next_unused_index(md, internal)
for address_type in (0, 1):
next_unused = self.get_next_unused_index(md, address_type)
for index in range(next_unused):
addresses.add(self.get_addr(md, internal, index))
addresses.add(self.get_addr(md, address_type, index))
for index in range(self.gap_limit):
addresses.add(self.get_new_addr(md, internal))
addresses.add(self.get_new_addr(md, address_type))
# reset the indices to the value we had before the
# new address calls:
self.set_next_index(md, internal, next_unused)
saved_indices[md][internal] = next_unused
self.set_next_index(md, address_type, next_unused)
saved_indices[md][address_type] = next_unused
# include any imported addresses
for path in self.yield_imported_paths(md):
addresses.add(self.get_address_from_path(path))
if isinstance(self.wallet, FidelityBondMixin):
md = FidelityBondMixin.FIDELITY_BOND_MIXDEPTH
address_type = FidelityBondMixin.BIP32_TIMELOCK_ID
saved_indices[md] += [0]
next_unused = self.get_next_unused_index(md, address_type)
for index in range(next_unused):
for timenumber in range(FidelityBondMixin.TIMENUMBERS_PER_PUBKEY):
addresses.add(self.get_addr(md, address_type, index, timenumber))
for index in range(self.gap_limit // FidelityBondMixin.TIMELOCK_GAP_LIMIT_REDUCTION_FACTOR):
index += next_unused
assert self.wallet.get_index_cache_and_increment(md, address_type) == index
for timenumber in range(FidelityBondMixin.TIMENUMBERS_PER_PUBKEY):
self.wallet.get_script_and_update_map(md, address_type, index, timenumber)
addresses.add(self.get_addr(md, address_type, index, timenumber))
self.wallet.set_next_index(md, address_type, next_unused)
return addresses, saved_indices
def collect_addresses_gap(self, gap_limit=None):
@ -742,11 +842,22 @@ class WalletService(Service):
addresses = set()
for md in range(self.max_mixdepth + 1):
for internal in (True, False):
old_next = self.get_next_unused_index(md, internal)
for address_type in (1, 0):
old_next = self.get_next_unused_index(md, address_type)
for index in range(gap_limit):
addresses.add(self.get_new_addr(md, internal))
self.set_next_index(md, internal, old_next)
addresses.add(self.get_new_addr(md, address_type))
self.set_next_index(md, address_type, old_next)
if isinstance(self.wallet, FidelityBondMixin):
md = FidelityBondMixin.FIDELITY_BOND_MIXDEPTH
address_type = FidelityBondMixin.BIP32_TIMELOCK_ID
old_next = self.get_next_unused_index(md, address_type)
for ii in range(gap_limit // FidelityBondMixin.TIMELOCK_GAP_LIMIT_REDUCTION_FACTOR):
index = self.wallet.get_index_cache_and_increment(md, address_type)
for timenumber in range(FidelityBondMixin.TIMENUMBERS_PER_PUBKEY):
self.wallet.get_script_and_update_map(md, address_type, index, timenumber)
addresses.add(self.get_addr(md, address_type, index, timenumber))
self.set_next_index(md, address_type, old_next)
return addresses

293
jmclient/jmclient/wallet_utils.py

@ -5,6 +5,7 @@ import sys
import sqlite3
import binascii
from datetime import datetime
from calendar import timegm
from optparse import OptionParser
from numbers import Integral
from collections import Counter
@ -12,12 +13,13 @@ from itertools import islice
from jmclient import (get_network, WALLET_IMPLEMENTATIONS, Storage, podle,
jm_single, BitcoinCoreInterface, WalletError,
VolatileStorage, StoragePasswordError, is_segwit_mode, SegwitLegacyWallet,
LegacyWallet, SegwitWallet, is_native_segwit_mode, load_program_config,
add_base_options, check_regtest)
LegacyWallet, SegwitWallet, FidelityBondMixin, FidelityBondWatchonlyWallet,
is_native_segwit_mode, load_program_config, add_base_options, check_regtest)
from jmclient.wallet_service import WalletService
from jmbase.support import get_password, jmprint, EXIT_FAILURE, EXIT_ARGERROR
from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH
from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH, \
TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS
from .output import fmt_utxo
import jmbitcoin as btc
@ -42,8 +44,12 @@ def get_wallettool_parser():
'(dumpprivkey) Export a single private key, specify an hd wallet path\n'
'(signmessage) Sign a message with the private key from an address in \n'
'the wallet. Use with -H and specify an HD wallet path for the address.\n'
'(freeze) Freeze or un-freeze a specific utxo. Specify mixdepth with -m.')
parser = OptionParser(usage='usage: %prog [options] [wallet file] [method]',
'(freeze) Freeze or un-freeze a specific utxo. Specify mixdepth with -m.\n'
'(gettimelockaddress) Obtain a timelocked address. Argument is locktime value as yyyy-mm. For example `2021-03`\n'
'(addtxoutproof) Add a tx out proof as metadata to a burner transaction. Specify path with '
'-H and proof which is output of Bitcoin Core\'s RPC call gettxoutproof\n'
'(createwatchonly) Create a watch-only fidelity bond wallet')
parser = OptionParser(usage='usage: %prog [options] [wallet file] [method] [args..]',
description=description)
add_base_options(parser)
parser.add_option('-p',
@ -162,13 +168,15 @@ class WalletViewBase(object):
return "{0:.08f}".format(self.get_balance(include_unconf))
class WalletViewEntry(WalletViewBase):
def __init__(self, wallet_path_repr, account, forchange, aindex, addr, amounts,
def __init__(self, wallet_path_repr, account, address_type, aindex, addr, amounts,
used = 'new', serclass=str, priv=None, custom_separator=None):
super(WalletViewEntry, self).__init__(wallet_path_repr, serclass=serclass,
custom_separator=custom_separator)
self.account = account
assert forchange in [0, 1, -1]
self.forchange =forchange
assert address_type in [SegwitWallet.BIP32_EXT_ID,
SegwitWallet.BIP32_INT_ID, -1, FidelityBondMixin.BIP32_TIMELOCK_ID,
FidelityBondMixin.BIP32_BURN_ID]
self.address_type = address_type
assert isinstance(aindex, Integral)
assert aindex >= 0
self.aindex = aindex
@ -212,15 +220,23 @@ class WalletViewEntry(WalletViewBase):
ed += self.separator + self.serclass(self.private_key)
return self.serclass(ed)
class WalletViewEntryBurnOutput(WalletViewEntry):
# balance in burn outputs shouldnt be counted
# towards the total balance
def get_balance(self, include_unconf=True):
return 0
class WalletViewBranch(WalletViewBase):
def __init__(self, wallet_path_repr, account, forchange, branchentries=None,
def __init__(self, wallet_path_repr, account, address_type, branchentries=None,
xpub=None, serclass=str, custom_separator=None):
super(WalletViewBranch, self).__init__(wallet_path_repr, children=branchentries,
serclass=serclass,
custom_separator=custom_separator)
self.account = account
assert forchange in [0, 1, -1]
self.forchange = forchange
assert address_type in [SegwitWallet.BIP32_EXT_ID,
SegwitWallet.BIP32_INT_ID, -1, FidelityBondMixin.BIP32_TIMELOCK_ID,
FidelityBondMixin.BIP32_BURN_ID]
self.address_type = address_type
if xpub:
assert xpub.startswith('xpub') or xpub.startswith('tpub')
self.xpub = xpub if xpub else ""
@ -238,8 +254,8 @@ class WalletViewBranch(WalletViewBase):
return self.serclass(entryseparator.join(lines))
def serialize_branch_header(self):
start = "external addresses" if self.forchange == 0 else "internal addresses"
if self.forchange == -1:
start = "external addresses" if self.address_type == 0 else "internal addresses"
if self.address_type == -1:
start = "Imported keys"
return self.serclass(self.separator.join([start, self.wallet_path_repr,
self.xpub]))
@ -254,7 +270,7 @@ class WalletViewAccount(WalletViewBase):
self.account_name = account_name
self.xpub = xpub
if branches:
assert len(branches) in [2, 3] #3 if imported keys
assert len(branches) in [2, 3, 4] #3 if imported keys, 4 if fidelity bonds
assert all([isinstance(x, WalletViewBranch) for x in branches])
self.branches = branches
@ -418,33 +434,105 @@ def wallet_display(wallet_service, showprivkey, displayall=False,
utxos = wallet_service.get_utxos_by_mixdepth(include_disabled=True, hexfmt=False)
for m in range(wallet_service.mixdepth + 1):
branchlist = []
for forchange in [0, 1]:
for address_type in [0, 1]:
entrylist = []
if forchange == 0:
if address_type == 0:
# users would only want to hand out the xpub for externals
xpub_key = wallet_service.get_bip32_pub_export(m, forchange)
xpub_key = wallet_service.get_bip32_pub_export(m, address_type)
else:
xpub_key = ""
unused_index = wallet_service.get_next_unused_index(m, forchange)
unused_index = wallet_service.get_next_unused_index(m, address_type)
for k in range(unused_index + wallet_service.gap_limit):
path = wallet_service.get_path(m, forchange, k)
path = wallet_service.get_path(m, address_type, k)
addr = wallet_service.get_address_from_path(path)
balance, used = get_addr_status(
path, utxos[m], k >= unused_index, forchange)
path, utxos[m], k >= unused_index, address_type)
if showprivkey:
privkey = wallet_service.get_wif_path(path)
else:
privkey = ''
if (displayall or balance > 0 or
(used == 'new' and forchange == 0)):
(used == 'new' and address_type == 0)):
entrylist.append(WalletViewEntry(
wallet_service.get_path_repr(path), m, forchange, k, addr,
wallet_service.get_path_repr(path), m, address_type, k, addr,
[balance, balance], priv=privkey, used=used))
wallet_service.set_next_index(m, forchange, unused_index)
path = wallet_service.get_path_repr(wallet_service.get_path(m, forchange))
branchlist.append(WalletViewBranch(path, m, forchange, entrylist,
wallet_service.set_next_index(m, address_type, unused_index)
path = wallet_service.get_path_repr(wallet_service.get_path(m, address_type))
branchlist.append(WalletViewBranch(path, m, address_type, entrylist,
xpub=xpub_key))
if m == FidelityBondMixin.FIDELITY_BOND_MIXDEPTH and \
isinstance(wallet_service.wallet, FidelityBondMixin):
address_type = FidelityBondMixin.BIP32_TIMELOCK_ID
unused_index = wallet_service.get_next_unused_index(m, address_type)
timelocked_gaplimit = (wallet_service.wallet.gap_limit
// FidelityBondMixin.TIMELOCK_GAP_LIMIT_REDUCTION_FACTOR)
entrylist = []
for k in range(unused_index + timelocked_gaplimit):
for timenumber in range(FidelityBondMixin.TIMENUMBERS_PER_PUBKEY):
path = wallet_service.get_path(m, address_type, k, timenumber)
addr = wallet_service.get_address_from_path(path)
timelock = datetime.utcfromtimestamp(path[-1])
balance = sum([utxodata["value"] for utxo, utxodata in
iteritems(utxos[m]) if path == utxodata["path"]])
status = timelock.strftime("%Y-%m-%d") + " [" + (
"LOCKED" if datetime.now() < timelock else "UNLOCKED") + "]"
privkey = ""
if showprivkey:
privkey = wallet_service.get_wif_path(path)
if displayall or balance > 0:
entrylist.append(WalletViewEntry(
wallet_service.get_path_repr(path), m, address_type, k,
addr, [balance, balance], priv=privkey, used=status))
xpub_key = wallet_service.get_bip32_pub_export(m, address_type)
path = wallet_service.get_path_repr(wallet_service.get_path(m, address_type))
branchlist.append(WalletViewBranch(path, m, address_type, entrylist,
xpub=xpub_key))
entrylist = []
address_type = FidelityBondMixin.BIP32_BURN_ID
unused_index = wallet_service.get_next_unused_index(m, address_type)
burner_outputs = wallet_service.wallet.get_burner_outputs()
wallet_service.set_next_index(m, address_type, unused_index +
wallet_service.wallet.gap_limit, force=True)
for k in range(unused_index + wallet_service.wallet.gap_limit):
path = wallet_service.get_path(m, address_type, k)
path_repr = wallet_service.get_path_repr(path)
path_repr_b = path_repr.encode()
privkey, engine = wallet_service._get_key_from_path(path)
pubkey = engine.privkey_to_pubkey(privkey)
pubkeyhash = btc.bin_hash160(pubkey)
output = "BURN-" + binascii.hexlify(pubkeyhash).decode()
balance = 0
status = "no transaction"
if path_repr_b in burner_outputs:
txhex, blockheight, merkle_branch, blockindex = burner_outputs[path_repr_b]
txhex = binascii.hexlify(txhex).decode()
txd = btc.deserialize(txhex)
assert len(txd["outs"]) == 1
balance = txd["outs"][0]["value"]
script = binascii.unhexlify(txd["outs"][0]["script"])
assert script[0] == 0x6a #OP_RETURN
tx_pubkeyhash = script[2:]
assert tx_pubkeyhash == pubkeyhash
status = btc.txhash(txhex) + (" [NO MERKLE PROOF]" if
merkle_branch == FidelityBondMixin.MERKLE_BRANCH_UNAVAILABLE else "")
privkey = (wallet_service.get_wif_path(path) if showprivkey else "")
if displayall or balance > 0:
entrylist.append(WalletViewEntryBurnOutput(path_repr, m,
address_type, k, output, [balance, balance],
priv=privkey, used=status))
wallet_service.set_next_index(m, address_type, unused_index)
xpub_key = wallet_service.get_bip32_pub_export(m, address_type)
path = wallet_service.get_path_repr(wallet_service.get_path(m, address_type))
branchlist.append(WalletViewBranch(path, m, address_type, entrylist,
xpub=xpub_key))
ipb = get_imported_privkey_branch(wallet_service, m, showprivkey)
if ipb:
branchlist.append(ipb)
@ -468,8 +556,8 @@ def cli_get_wallet_passphrase_check():
return False
return password
def cli_get_wallet_file_name():
return input('Input wallet file name (default: wallet.jmdat): ')
def cli_get_wallet_file_name(defaultname="wallet.jmdat"):
return input('Input wallet file name (default: ' + defaultname + '): ')
def cli_display_user_words(words, mnemonic_extension):
text = 'Write down this wallet recovery mnemonic\n\n' + words +'\n'
@ -498,11 +586,19 @@ def cli_get_mnemonic_extension():
"info")
return input("Enter mnemonic extension: ")
def cli_do_support_fidelity_bonds():
uin = input("Would you like this wallet to support fidelity bonds? "
"write 'n' if you don't know what this is (y/n): ")
if len(uin) == 0 or uin[0] != 'y':
jmprint("Not supporting fidelity bonds", "info")
return False
else:
return True
def wallet_generate_recover_bip39(method, walletspath, default_wallet_name,
display_seed_callback, enter_seed_callback, enter_wallet_password_callback,
enter_wallet_file_name_callback, enter_if_use_seed_extension,
enter_seed_extension_callback, mixdepth=DEFAULT_MIXDEPTH):
enter_seed_extension_callback, enter_do_support_fidelity_bonds, mixdepth=DEFAULT_MIXDEPTH):
entropy = None
mnemonic_extension = None
if method == "generate":
@ -536,7 +632,13 @@ def wallet_generate_recover_bip39(method, walletspath, default_wallet_name,
wallet_name = default_wallet_name
wallet_path = os.path.join(walletspath, wallet_name)
wallet = create_wallet(wallet_path, password, mixdepth,
# disable creating fidelity bond wallets for now until the
# rest of the fidelity bond feature is created
#support_fidelity_bonds = enter_do_support_fidelity_bonds()
support_fidelity_bonds = False
wallet_cls = get_wallet_cls(get_configured_wallet_type(support_fidelity_bonds))
wallet = create_wallet(wallet_path, password, mixdepth, wallet_cls,
entropy=entropy,
entropy_extension=mnemonic_extension)
mnemonic, mnext = wallet.get_mnemonic_words()
@ -554,7 +656,7 @@ def wallet_generate_recover(method, walletspath,
default_wallet_name, cli_display_user_words, cli_user_mnemonic_entry,
cli_get_wallet_passphrase_check, cli_get_wallet_file_name,
cli_do_use_mnemonic_extension, cli_get_mnemonic_extension,
mixdepth=mixdepth)
cli_do_support_fidelity_bonds, mixdepth=mixdepth)
entropy = None
if method == 'recover':
@ -1040,29 +1142,99 @@ def wallet_freezeutxo(wallet_service, md, display_callback=None, info_callback=N
.format(fmt_utxo((txid, index))))
return "Done"
def get_wallet_type():
def wallet_gettimelockaddress(wallet, locktime_string):
if not isinstance(wallet, FidelityBondMixin):
jmprint("Error: not a fidelity bond wallet", "error")
return ""
m = FidelityBondMixin.FIDELITY_BOND_MIXDEPTH
address_type = FidelityBondMixin.BIP32_TIMELOCK_ID
index = wallet.get_next_unused_index(m, address_type)
lock_datetime = datetime.strptime(locktime_string, "%Y-%m")
timenumber = FidelityBondMixin.timestamp_to_time_number(timegm(
lock_datetime.timetuple()))
path = wallet.get_path(m, address_type, index, timenumber)
jmprint("path = " + wallet.get_path_repr(path), "info")
jmprint("Coins sent to this address will be not be spendable until "
+ lock_datetime.strftime("%B %Y") + ". Full date: "
+ str(lock_datetime))
addr = wallet.get_address_from_path(path)
return addr
def wallet_addtxoutproof(wallet_service, hdpath, txoutproof):
if not isinstance(wallet_service.wallet, FidelityBondMixin):
jmprint("Error: not a fidelity bond wallet", "error")
return ""
path = hdpath.encode()
if path not in wallet_service.wallet.get_burner_outputs():
jmprint("Error: unknown burner transaction with on that path", "error")
return ""
txhex, block_height, old_merkle_branch, block_index = \
wallet_service.wallet.get_burner_outputs()[path]
new_merkle_branch = jm_single().bc_interface.core_proof_to_merkle_branch(txoutproof)
txhex = binascii.hexlify(txhex).decode()
txid = btc.txhash(txhex)
if not jm_single().bc_interface.verify_tx_merkle_branch(txid, block_height,
new_merkle_branch):
jmprint("Error: tx out proof invalid", "error")
return ""
wallet_service.wallet.add_burner_output(hdpath, txhex, block_height,
new_merkle_branch, block_index)
return "Done"
def wallet_createwatchonly(wallet_root_path, master_pub_key):
wallet_name = cli_get_wallet_file_name(defaultname="watchonly.jmdat")
if not wallet_name:
DEFAULT_WATCHONLY_WALLET_NAME = "watchonly.jmdat"
wallet_name = DEFAULT_WATCHONLY_WALLET_NAME
wallet_path = os.path.join(wallet_root_path, wallet_name)
password = cli_get_wallet_passphrase_check()
if not password:
return ""
entropy = FidelityBondMixin.get_xpub_from_fidelity_bond_master_pub_key(master_pub_key)
if not entropy:
jmprint("Error with provided master pub key", "error")
return ""
entropy = entropy.encode()
wallet = create_wallet(wallet_path, password,
max_mixdepth=FidelityBondMixin.FIDELITY_BOND_MIXDEPTH,
wallet_cls=FidelityBondWatchonlyWallet, entropy=entropy)
return "Done"
def get_configured_wallet_type(support_fidelity_bonds):
configured_type = TYPE_P2PKH
if is_segwit_mode():
if is_native_segwit_mode():
return TYPE_P2WPKH
return TYPE_P2SH_P2WPKH
return TYPE_P2PKH
configured_type = TYPE_P2WPKH
else:
configured_type = TYPE_P2SH_P2WPKH
if not support_fidelity_bonds:
return configured_type
def get_wallet_cls(wtype=None):
if wtype is None:
wtype = get_wallet_type()
if configured_type == TYPE_P2SH_P2WPKH:
return TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS
else:
raise ValueError("Fidelity bonds not supported with the configured "
"options of segwit and native. Edit joinmarket.cfg")
def get_wallet_cls(wtype):
cls = WALLET_IMPLEMENTATIONS.get(wtype)
if not cls:
raise WalletError("No wallet implementation found for type {}."
"".format(wtype))
return cls
def create_wallet(path, password, max_mixdepth, wallet_cls=None, **kwargs):
def create_wallet(path, password, max_mixdepth, wallet_cls, **kwargs):
storage = Storage(path, password, create=True)
wallet_cls = wallet_cls or get_wallet_cls()
wallet_cls.initialize(storage, get_network(), max_mixdepth=max_mixdepth,
**kwargs)
storage.save()
@ -1198,13 +1370,14 @@ def wallet_tool_main(wallet_root_path):
check_regtest(blockchain_start=False)
# full path to the wallets/ subdirectory in the user data area:
wallet_root_path = os.path.join(jm_single().datadir, wallet_root_path)
noseed_methods = ['generate', 'recover']
noseed_methods = ['generate', 'recover', 'createwatchonly']
methods = ['display', 'displayall', 'summary', 'showseed', 'importprivkey',
'history', 'showutxos', 'freeze']
'history', 'showutxos', 'freeze', 'gettimelockaddress', 'addtxoutproof']
methods.extend(noseed_methods)
noscan_methods = ['showseed', 'importprivkey', 'dumpprivkey', 'signmessage']
readonly_methods = ['display', 'displayall', 'summary', 'showseed',
'history', 'showutxos', 'dumpprivkey', 'signmessage']
'history', 'showutxos', 'dumpprivkey', 'signmessage',
'gettimelockaddress']
if len(args) < 1:
parser.error('Needs a wallet file or method')
@ -1224,6 +1397,11 @@ def wallet_tool_main(wallet_root_path):
method = ('display' if len(args) == 1 else args[1].lower())
read_only = method in readonly_methods
#special case needed for fidelity bond burner outputs
#maybe theres a better way to do this
if options.recoversync:
read_only = False
wallet = open_test_wallet_maybe(
wallet_path, seed, options.mixdepth, read_only=read_only,
wallet_password_stdin=options.wallet_password_stdin, gap_limit=options.gaplimit)
@ -1278,9 +1456,28 @@ def wallet_tool_main(wallet_root_path):
map_key_type(options.key_type))
return "Key import completed."
elif method == "signmessage":
if len(args) < 3:
jmprint('Must provide message to sign', "error")
sys.exit(EXIT_ARGERROR)
return wallet_signmessage(wallet_service, options.hd_path, args[2])
elif method == "freeze":
return wallet_freezeutxo(wallet_service, options.mixdepth)
elif method == "gettimelockaddress":
if len(args) < 3:
jmprint('Must have locktime value yyyy-mm. For example 2021-03', "error")
sys.exit(EXIT_ARGERROR)
return wallet_gettimelockaddress(wallet_service.wallet, args[2])
elif method == "addtxoutproof":
if len(args) < 3:
jmprint('Must have txout proof, which is the output of Bitcoin '
+ 'Core\'s RPC call gettxoutproof', "error")
sys.exit(EXIT_ARGERROR)
return wallet_addtxoutproof(wallet_service, options.hd_path, args[2])
elif method == "createwatchonly":
if len(args) < 2:
jmprint("args: [master public key]", "error")
sys.exit(EXIT_ARGERROR)
return wallet_createwatchonly(wallet_root_path, args[1])
else:
parser.error("Unknown wallet-tool method: " + method)
sys.exit(EXIT_ARGERROR)
@ -1296,15 +1493,15 @@ if __name__ == "__main__":
acctlist = []
for a in accounts:
branches = []
for forchange in range(2):
for address_type in range(2):
entries = []
for i in range(4):
entries.append(WalletViewEntry(rootpath, a, forchange,
entries.append(WalletViewEntry(rootpath, a, address_type,
i, "DUMMYADDRESS"+str(i+a),
[i*10000000, i*10000000]))
branches.append(WalletViewBranch(rootpath,
a, forchange, branchentries=entries,
xpub="xpubDUMMYXPUB"+str(a+forchange)))
a, address_type, branchentries=entries,
xpub="xpubDUMMYXPUB"+str(a+address_type)))
acctlist.append(WalletViewAccount(rootpath, a, branches=branches))
wallet = WalletView(rootpath + "/" + str(walletbranch),
accounts=acctlist)

10
jmclient/test/commontest.py

@ -209,3 +209,13 @@ def interact(process, inputs, expected):
for i, inp in enumerate(inputs):
process.expect(expected[i])
process.sendline(inp)
def ensure_bip65_activated():
#on regtest bip65 activates on height 1351
#https://github.com/bitcoin/bitcoin/blob/1d1f8bbf57118e01904448108a104e20f50d2544/src/chainparams.cpp#L262
BIP65Height = 1351
current_height = jm_single().bc_interface.rpc("getblockchaininfo", [])["blocks"]
until_bip65_activation = BIP65Height - current_height + 1
if until_bip65_activation > 0:
jm_single().bc_interface.tick_forward_chain(until_bip65_activation)

52
jmclient/test/test_tx_creation.py

@ -5,7 +5,8 @@ network to check validity.'''
import time
import binascii
import struct
from commontest import make_wallets, make_sign_and_push
from binascii import unhexlify
from commontest import make_wallets, make_sign_and_push, ensure_bip65_activated
import jmbitcoin as bitcoin
import pytest
@ -190,7 +191,7 @@ def test_spend_p2sh_utxos(setup_tx_creation):
tx = bitcoin.mktx(ins, outs)
sigs = []
for priv in privs[:2]:
sigs.append(bitcoin.multisign(tx, 0, script, binascii.hexlify(priv).decode('ascii')))
sigs.append(bitcoin.get_p2sh_signature(tx, 0, script, binascii.hexlify(priv).decode('ascii')))
tx = bitcoin.apply_multisignatures(tx, 0, script, sigs)
txid = jm_single().bc_interface.pushtx(tx)
assert txid
@ -244,8 +245,8 @@ def test_spend_p2wsh(setup_tx_creation):
privs = [binascii.hexlify(priv).decode('ascii') for priv in privs]
pubs = [bitcoin.privkey_to_pubkey(priv) for priv in privs]
redeemScripts = [bitcoin.mk_multisig_script(pubs[i:i+2], 2) for i in [0, 2]]
scriptPubKeys = [bitcoin.pubkeys_to_p2wsh_script(pubs[i:i+2]) for i in [0, 2]]
addresses = [bitcoin.pubkeys_to_p2wsh_address(pubs[i:i+2]) for i in [0, 2]]
scriptPubKeys = [bitcoin.pubkeys_to_p2wsh_multisig_script(pubs[i:i+2]) for i in [0, 2]]
addresses = [bitcoin.pubkeys_to_p2wsh_multisig_address(pubs[i:i+2]) for i in [0, 2]]
#pay into it
wallet_service = make_wallets(1, [[3, 0, 0, 0, 0]], 3)[0]['wallet']
wallet_service.sync_wallet(fast=True)
@ -268,7 +269,7 @@ def test_spend_p2wsh(setup_tx_creation):
sigs = []
for priv in privs[i*2:i*2+2]:
# sign input j with each of 2 keys
sig = bitcoin.multisign(tx, i, redeemScripts[i], priv, amount=amount)
sig = bitcoin.get_p2sh_signature(tx, i, redeemScripts[i], priv, amount=amount)
sigs.append(sig)
# check that verify_tx_input correctly validates;
assert bitcoin.verify_tx_input(tx, i, scriptPubKeys[i], sig,
@ -278,6 +279,47 @@ def test_spend_p2wsh(setup_tx_creation):
txid = jm_single().bc_interface.pushtx(tx)
assert txid
def test_spend_freeze_script(setup_tx_creation):
ensure_bip65_activated()
wallet_service = make_wallets(1, [[3, 0, 0, 0, 0]], 3)[0]['wallet']
wallet_service.sync_wallet(fast=True)
mediantime = jm_single().bc_interface.rpc("getblockchaininfo", [])["mediantime"]
timeoffset_success_tests = [(2, False), (-60*60*24*30, True), (60*60*24*30, False)]
for timeoffset, required_success in timeoffset_success_tests:
#generate keypair
priv = "aa"*32 + "01"
pub = unhexlify(bitcoin.privkey_to_pubkey(priv))
addr_locktime = mediantime + timeoffset
redeem_script = bitcoin.mk_freeze_script(pub, addr_locktime)
script_pub_key = bitcoin.redeem_script_to_p2wsh_script(redeem_script)
regtest_vbyte = 100
addr = bitcoin.script_to_address(script_pub_key, vbyte=regtest_vbyte)
#fund frozen funds address
amount = 100000000
funding_ins_full = wallet_service.select_utxos(0, amount)
funding_txid = make_sign_and_push(funding_ins_full, wallet_service, amount, output_addr=addr)
assert funding_txid
#spend frozen funds
frozen_in = funding_txid + ":0"
output_addr = wallet_service.get_internal_addr(1)
miner_fee = 5000
outs = [{'value': amount - miner_fee, 'address': output_addr}]
tx = bitcoin.mktx([frozen_in], outs, locktime=addr_locktime+1)
i = 0
sig = bitcoin.get_p2sh_signature(tx, i, redeem_script, priv, amount)
assert bitcoin.verify_tx_input(tx, i, script_pub_key, sig, pub,
scriptCode=redeem_script, amount=amount)
tx = bitcoin.apply_freeze_signature(tx, i, redeem_script, sig)
push_success = jm_single().bc_interface.pushtx(tx)
assert push_success == required_success
@pytest.fixture(scope="module")
def setup_tx_creation():

163
jmclient/test/test_wallet.py

@ -6,12 +6,13 @@ from binascii import hexlify, unhexlify
import pytest
import jmbitcoin as btc
from commontest import binarize_tx
from commontest import binarize_tx, ensure_bip65_activated
from jmbase import get_log
from jmclient import load_test_config, jm_single, \
SegwitLegacyWallet,BIP32Wallet, BIP49Wallet, LegacyWallet,\
VolatileStorage, get_network, cryptoengine, WalletError,\
SegwitWallet, WalletService
SegwitWallet, WalletService, SegwitLegacyWalletFidelityBonds,\
FidelityBondMixin, FidelityBondWatchonlyWallet, wallet_gettimelockaddress
from test_blockchaininterface import sync_test_wallet
testdir = os.path.dirname(os.path.realpath(__file__))
@ -239,6 +240,72 @@ def test_bip32_addresses_p2sh_p2wpkh(setup_wallet, mixdepth, internal, index, ad
assert wif == wallet.get_wif(mixdepth, internal, index)
assert address == wallet.get_addr(mixdepth, internal, index)
@pytest.mark.parametrize('index,timenumber,address,wif', [
[0, 0, 'bcrt1qndcqwedwa4lu77ryqpvp738d6p034a2fv8mufw3pw5smfcn39sgqpesn76', 'cST4g5R3mKp44K4J8PRVyys4XJu6EFavZyssq67PJKCnbhjdEdBY'],
[0, 50, 'bcrt1q73zhrfcu0ttkk4er9esrmvnpl6wpzhny5aly97jj9nw52agf8ncqjv8rda', 'cST4g5R3mKp44K4J8PRVyys4XJu6EFavZyssq67PJKCnbhjdEdBY'],
[5, 0, 'bcrt1qz5208jdm6399ja309ra28d0a34qlt0859u77uxc94v5mgk7auhtssau4pw', 'cRnUaBYTmyZURPe72YCrtvgxpBMvLKPZaCoXvKuWRPMryeJeAZx2'],
[9, 1, 'bcrt1qa7pd6qnadpmlm29vtvqnykalc34tr33eclaz7eeqal59n4gwr28qwnka2r', 'cQCxEPCWMwXVB16zCikDBTXMUccx6ioHQipPhYEp1euihkJUafyD']
])
def test_bip32_timelocked_addresses(setup_wallet, index, timenumber, address, wif):
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
entropy = unhexlify('2e0339ba89b4a1272cdf78b27ee62669ee01992a59e836e2807051be128ca817')
storage = VolatileStorage()
SegwitLegacyWalletFidelityBonds.initialize(
storage, get_network(), entropy=entropy, max_mixdepth=1)
wallet = SegwitLegacyWalletFidelityBonds(storage)
mixdepth = FidelityBondMixin.FIDELITY_BOND_MIXDEPTH
address_type = FidelityBondMixin.BIP32_TIMELOCK_ID
#wallet needs to know about the script beforehand
wallet.get_script_and_update_map(mixdepth, address_type, index, timenumber)
assert address == wallet.get_addr(mixdepth, address_type, index, timenumber)
assert wif == wallet.get_wif_path(wallet.get_path(mixdepth, address_type, index, timenumber))
@pytest.mark.parametrize('timenumber,locktime_string', [
[0, "2020-01"],
[20, "2021-09"],
[100, "2028-05"],
[150, "2032-07"],
[350, "2049-03"]
])
def test_gettimelockaddress_method(setup_wallet, timenumber, locktime_string):
storage = VolatileStorage()
SegwitLegacyWalletFidelityBonds.initialize(storage, get_network())
wallet = SegwitLegacyWalletFidelityBonds(storage)
m = FidelityBondMixin.FIDELITY_BOND_MIXDEPTH
address_type = FidelityBondMixin.BIP32_TIMELOCK_ID
index = wallet.get_next_unused_index(m, address_type)
script = wallet.get_script_and_update_map(m, address_type, index,
timenumber)
addr = wallet.script_to_addr(script)
addr_from_method = wallet_gettimelockaddress(wallet, locktime_string)
assert addr == addr_from_method
@pytest.mark.parametrize('index,wif', [
[0, 'cMg9eH3fW2JDSyggvXucjmECRwiheCMDo2Qik8y1keeYaxynzrYa'],
[9, 'cURA1Qgxhd7QnhhwxCnCHD4pZddVrJdu2BkTdzNaTp9owRSkUvPy'],
[50, 'cRTaHZ1eezb8s6xsT2V7EAevYToQMi7cxQD9vgFZzaJZDfhMhf3c']
])
def test_bip32_burn_keys(setup_wallet, index, wif):
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
entropy = unhexlify('2e0339ba89b4a1272cdf78b27ee62669ee01992a59e836e2807051be128ca817')
storage = VolatileStorage()
SegwitLegacyWalletFidelityBonds.initialize(
storage, get_network(), entropy=entropy, max_mixdepth=1)
wallet = SegwitLegacyWalletFidelityBonds(storage)
mixdepth = FidelityBondMixin.FIDELITY_BOND_MIXDEPTH
address_type = FidelityBondMixin.BIP32_BURN_ID
#advance index_cache enough
wallet.set_next_index(mixdepth, address_type, index, force=True)
assert wif == wallet.get_wif_path(wallet.get_path(mixdepth, address_type, index))
def test_import_key(setup_wallet):
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
@ -331,6 +398,29 @@ def test_signing_simple(setup_wallet, wallet_cls, type_check):
txout = jm_single().bc_interface.pushtx(btc.serialize(tx))
assert txout
def test_timelocked_output_signing(setup_wallet):
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
ensure_bip65_activated()
storage = VolatileStorage()
SegwitLegacyWalletFidelityBonds.initialize(storage, get_network())
wallet = SegwitLegacyWalletFidelityBonds(storage)
index = 0
timenumber = 0
script = wallet.get_script_and_update_map(
FidelityBondMixin.FIDELITY_BOND_MIXDEPTH,
FidelityBondMixin.BIP32_TIMELOCK_ID, index, timenumber)
utxo = fund_wallet_addr(wallet, wallet.script_to_addr(script))
timestamp = wallet._time_number_to_timestamp(timenumber)
tx = btc.deserialize(btc.mktx(['{}:{}'.format(
hexlify(utxo[0]).decode('ascii'), utxo[1])],
[btc.p2sh_scriptaddr(b"\x00",magicbyte=196) + ':' + str(10**8 - 9000)],
locktime=timestamp+1))
tx = wallet.sign_tx(tx, {0: (script, 10**8)})
txout = jm_single().bc_interface.pushtx(btc.serialize(tx))
assert txout
def test_get_bbm(setup_wallet):
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
amount = 10**8
@ -520,7 +610,7 @@ def test_set_next_index(setup_wallet):
def test_path_repr(setup_wallet):
wallet = get_populated_wallet()
path = wallet.get_path(2, False, 0)
path = wallet.get_path(2, BIP32Wallet.ADDRESS_TYPE_EXTERNAL, 0)
path_repr = wallet.get_path_repr(path)
path_new = wallet.path_repr_to_path(path_repr)
@ -537,6 +627,37 @@ def test_path_repr_imported(setup_wallet):
assert path_new == path
@pytest.mark.parametrize('timenumber,timestamp', [
[0, 1577836800],
[50, 1709251200],
[300, 2366841600],
[400, None], #too far in the future
[-1, None] #before epoch
])
def test_timenumber_to_timestamp(setup_wallet, timenumber, timestamp):
try:
implied_timestamp = FidelityBondMixin._time_number_to_timestamp(
timenumber)
assert implied_timestamp == timestamp
except ValueError:
#None means the timenumber is intentionally invalid
assert timestamp == None
@pytest.mark.parametrize('timestamp,timenumber', [
[1577836800, 0],
[1709251200, 50],
[2366841600, 300],
[1577836801, None], #not exactly midnight on first of month
[2629670400, None], #too far in future
[1575158400, None] #before epoch
])
def test_timestamp_to_timenumber(setup_wallet, timestamp, timenumber):
try:
implied_timenumber = FidelityBondMixin.timestamp_to_time_number(
timestamp)
assert implied_timenumber == timenumber
except ValueError:
assert timenumber == None
def test_wrong_wallet_cls(setup_wallet):
storage = VolatileStorage()
@ -658,6 +779,42 @@ def test_wallet_mixdepth_decrease(setup_wallet):
# because we explicitly ask for a specific mixdepth
assert utxo in new_wallet.select_utxos_(max_mixdepth, 10**7)
def test_watchonly_wallet(setup_wallet):
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
storage = VolatileStorage()
SegwitLegacyWalletFidelityBonds.initialize(storage, get_network())
wallet = SegwitLegacyWalletFidelityBonds(storage)
paths = [
"m/49'/1'/0'/0/0",
"m/49'/1'/0'/1/0",
"m/49'/1'/0'/2/0:1577836800",
"m/49'/1'/0'/2/0:2314051200"
]
burn_path = "m/49'/1'/0'/3/0"
scripts = [wallet.get_script_from_path(wallet.path_repr_to_path(path))
for path in paths]
privkey, engine = wallet._get_key_from_path(wallet.path_repr_to_path(burn_path))
burn_pubkey = engine.privkey_to_pubkey(privkey)
master_pub_key = wallet.get_bip32_pub_export(
FidelityBondMixin.FIDELITY_BOND_MIXDEPTH)
watchonly_storage = VolatileStorage()
entropy = FidelityBondMixin.get_xpub_from_fidelity_bond_master_pub_key(
master_pub_key).encode()
FidelityBondWatchonlyWallet.initialize(watchonly_storage, get_network(),
entropy=entropy)
watchonly_wallet = FidelityBondWatchonlyWallet(watchonly_storage)
watchonly_scripts = [watchonly_wallet.get_script_from_path(
watchonly_wallet.path_repr_to_path(path)) for path in paths]
privkey, engine = wallet._get_key_from_path(wallet.path_repr_to_path(burn_path))
watchonly_burn_pubkey = engine.privkey_to_pubkey(privkey)
for script, watchonly_script in zip(scripts, watchonly_scripts):
assert script == watchonly_script
assert burn_pubkey == watchonly_burn_pubkey
@pytest.fixture(scope='module')
def setup_wallet():

8
scripts/joinmarket-qt.py

@ -75,7 +75,8 @@ from jmclient import load_program_config, get_network, update_persist_config,\
get_tumble_log, restart_wait, tumbler_filter_orders_callback,\
wallet_generate_recover_bip39, wallet_display, get_utxos_enabled_disabled,\
NO_ROUNDING, get_max_cj_fee_values, get_default_max_absolute_fee, \
get_default_max_relative_fee, RetryableStorageError, add_base_options
get_default_max_relative_fee, RetryableStorageError, add_base_options, \
FidelityBondMixin
from qtsupport import ScheduleWizard, TumbleRestartWizard, config_tips,\
config_types, QtHandler, XStream, Buttons, OkButton, CancelButton,\
PasswordDialog, MyTreeWidget, JMQtMessageBox, BLUE_FG,\
@ -1712,6 +1713,8 @@ class JMMainWindow(QMainWindow):
return False
# only used for GUI display on regtest:
self.testwalletname = wallet.seed = str(firstarg)
if isinstance(wallet, FidelityBondMixin):
raise Exception("Fidelity bond wallets not supported by Qt")
if 'listunspent_args' not in jm_single().config.options('POLICY'):
jm_single().config.set('POLICY', 'listunspent_args', '[0]')
assert wallet, "No wallet loaded"
@ -1862,7 +1865,8 @@ class JMMainWindow(QMainWindow):
enter_wallet_password_callback=self.getPassword,
enter_wallet_file_name_callback=self.getWalletFileName,
enter_if_use_seed_extension=self.promptUseMnemonicExtension,
enter_seed_extension_callback=self.promptInputMnemonicExtension)
enter_seed_extension_callback=self.promptInputMnemonicExtension,
enter_do_support_fidelity_bonds=lambda: False)
if not success:
JMQtMessageBox(self, "Failed to create new wallet file.",

11
scripts/sendpayment.py

@ -12,8 +12,8 @@ from twisted.internet import reactor
import pprint
from jmclient import Taker, P2EPTaker, load_program_config, get_schedule,\
JMClientProtocolFactory, start_reactor, validate_address, jm_single,\
estimate_tx_fee, direct_send, WalletService,\
JMClientProtocolFactory, start_reactor, validate_address, is_burn_destination, \
jm_single, estimate_tx_fee, direct_send, WalletService,\
open_test_wallet_maybe, get_wallet_path, NO_ROUNDING, \
get_sendpayment_parser, get_max_cj_fee_values, check_regtest
from twisted.python.log import startLogging
@ -83,8 +83,13 @@ def main():
destaddr = args[2]
mixdepth = options.mixdepth
addr_valid, errormsg = validate_address(destaddr)
if not addr_valid:
command_to_burn = (is_burn_destination(destaddr) and sweeping and
options.makercount == 0 and not options.p2ep)
if not addr_valid and not command_to_burn:
jmprint('ERROR: Address invalid. ' + errormsg, "error")
if is_burn_destination(destaddr):
jmprint("The required options for burning coins are zero makers"
+ " (-N 0), sweeping (amount = 0) and not using P2EP", "info")
sys.exit(EXIT_ARGERROR)
if sweeping == False and amount < DUST_THRESHOLD:
jmprint('ERROR: Amount ' + btc.amount_to_str(amount) +

Loading…
Cancel
Save