forked from LBRYCommunity/lbry-sdk
finally generating fully signed tx submittable to lbrycrd
This commit is contained in:
parent
dcd8a6bb0e
commit
83958604d5
25 changed files with 1678 additions and 1973 deletions
88
lbrynet/tests/integration/test_wallet.py
Normal file
88
lbrynet/tests/integration/test_wallet.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
import time
|
||||
import shutil
|
||||
import logging
|
||||
import tempfile
|
||||
from binascii import hexlify
|
||||
|
||||
from twisted.internet import defer, reactor, threads
|
||||
from twisted.trial import unittest
|
||||
from orchstr8.wrapper import BaseLbryServiceStack
|
||||
|
||||
from lbrynet.core.call_later_manager import CallLaterManager
|
||||
from lbrynet.database.storage import SQLiteStorage
|
||||
|
||||
from lbrynet.wallet import set_wallet_manager
|
||||
from lbrynet.wallet.wallet import Wallet
|
||||
from lbrynet.wallet.manager import WalletManager
|
||||
from lbrynet.wallet.transaction import Transaction, Output
|
||||
from lbrynet.wallet.constants import COIN, REGTEST_CHAIN
|
||||
from lbrynet.wallet.hash import hash160_to_address, address_to_hash_160
|
||||
|
||||
|
||||
class WalletTestCase(unittest.TestCase):
|
||||
|
||||
VERBOSE = False
|
||||
|
||||
def setUp(self):
|
||||
logging.getLogger('lbrynet').setLevel(logging.INFO)
|
||||
self.data_path = tempfile.mkdtemp()
|
||||
self.db = SQLiteStorage(self.data_path)
|
||||
self.config = {
|
||||
'chain': REGTEST_CHAIN,
|
||||
'wallet_path': self.data_path,
|
||||
'default_servers': [('localhost', 50001)]
|
||||
}
|
||||
CallLaterManager.setup(reactor.callLater)
|
||||
self.service = BaseLbryServiceStack(self.VERBOSE)
|
||||
return self.service.startup()
|
||||
|
||||
def tearDown(self):
|
||||
CallLaterManager.stop()
|
||||
shutil.rmtree(self.data_path, ignore_errors=True)
|
||||
return self.service.shutdown()
|
||||
|
||||
@property
|
||||
def lbrycrd(self):
|
||||
return self.service.lbrycrd
|
||||
|
||||
|
||||
class StartupTests(WalletTestCase):
|
||||
|
||||
VERBOSE = True
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_balance(self):
|
||||
wallet = Wallet(chain=REGTEST_CHAIN)
|
||||
manager = WalletManager(self.config, wallet)
|
||||
set_wallet_manager(manager)
|
||||
yield manager.start()
|
||||
yield self.lbrycrd.generate(1)
|
||||
yield threads.deferToThread(time.sleep, 1)
|
||||
#yield wallet.network.on_header.first
|
||||
address = manager.get_least_used_receiving_address()
|
||||
sendtxid = yield self.lbrycrd.sendtoaddress(address, 2.5)
|
||||
yield self.lbrycrd.generate(1)
|
||||
#yield manager.wallet.history.on_transaction.
|
||||
yield threads.deferToThread(time.sleep, 10)
|
||||
tx = manager.ledger.transactions.values()[0]
|
||||
print(tx.to_python_source())
|
||||
print(address)
|
||||
output = None
|
||||
for txo in tx.outputs:
|
||||
other = hash160_to_address(txo.script.values['pubkey_hash'], 'regtest')
|
||||
if other == address:
|
||||
output = txo
|
||||
break
|
||||
|
||||
address2 = manager.get_least_used_receiving_address()
|
||||
tx = Transaction()
|
||||
tx.add_inputs([output.spend()])
|
||||
Output.pay_pubkey_hash(tx, 0, 2.49*COIN, address_to_hash_160(address2))
|
||||
print(tx.to_python_source())
|
||||
tx.sign(wallet)
|
||||
print(tx.to_python_source())
|
||||
|
||||
yield self.lbrycrd.decoderawtransaction(hexlify(tx.raw))
|
||||
yield self.lbrycrd.sendrawtransaction(hexlify(tx.raw))
|
||||
|
||||
yield manager.stop()
|
|
@ -71,7 +71,7 @@ class TestScriptTemplates(unittest.TestCase):
|
|||
|
||||
def test_push_data_mixed(self):
|
||||
self.assertEqual(parse(
|
||||
(PUSH_SINGLE('CEO'), PUSH_MANY('Devs'), PUSH_SINGLE(b'CTO'), PUSH_SINGLE(b'State')),
|
||||
(PUSH_SINGLE('CEO'), PUSH_MANY('Devs'), PUSH_SINGLE('CTO'), PUSH_SINGLE('State')),
|
||||
(b'jeremy', b'lex', b'amit', b'victor', b'jack', b'grin', b'NH')
|
||||
), {
|
||||
'CEO': b'jeremy',
|
||||
|
@ -114,7 +114,8 @@ class TestRedeemPubKeyHash(unittest.TestCase):
|
|||
def test_redeem_pubkey_hash_1(self):
|
||||
self.assertEqual(
|
||||
self.redeem_pubkey_hash(
|
||||
b'30450221009dc93f25184a8d483745cd3eceff49727a317c9bfd8be8d3d04517e9cdaf8dd502200e02dc5939cad9562d2b1f303f185957581c4851c98d497af281118825e18a8301',
|
||||
b'30450221009dc93f25184a8d483745cd3eceff49727a317c9bfd8be8d3d04517e9cdaf8dd502200e'
|
||||
b'02dc5939cad9562d2b1f303f185957581c4851c98d497af281118825e18a8301',
|
||||
b'025415a06514230521bff3aaface31f6db9d9bbc39bf1ca60a189e78731cfd4e1b'
|
||||
),
|
||||
'4830450221009dc93f25184a8d483745cd3eceff49727a317c9bfd8be8d3d04517e9cdaf8dd502200e02d'
|
||||
|
|
229
lbrynet/tests/unit/wallet/test_transaction.py
Normal file
229
lbrynet/tests/unit/wallet/test_transaction.py
Normal file
|
@ -0,0 +1,229 @@
|
|||
from binascii import hexlify, unhexlify
|
||||
from twisted.trial import unittest
|
||||
from lbrynet.wallet.constants import CENT
|
||||
from lbrynet.wallet.transaction import Transaction, Input, Output
|
||||
from lbrynet.wallet.manager import WalletManager
|
||||
from lbrynet.wallet import set_wallet_manager
|
||||
from lbrynet.wallet.bip32 import PrivateKey
|
||||
from lbrynet.wallet.mnemonic import Mnemonic
|
||||
|
||||
|
||||
NULL_HASH = '\x00'*32
|
||||
FEE_PER_BYTE = 50
|
||||
FEE_PER_CHAR = 200000
|
||||
|
||||
|
||||
class TestSizeAndFeeEstimation(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
set_wallet_manager(WalletManager({
|
||||
'fee_per_byte': FEE_PER_BYTE,
|
||||
'fee_per_name_char': FEE_PER_CHAR
|
||||
}))
|
||||
|
||||
@staticmethod
|
||||
def get_output():
|
||||
return Output.pay_pubkey_hash(Transaction(), 1, CENT, NULL_HASH)
|
||||
|
||||
@classmethod
|
||||
def get_input(cls):
|
||||
return cls.get_output().spend(fake=True)
|
||||
|
||||
@classmethod
|
||||
def get_transaction(cls):
|
||||
tx = Transaction()
|
||||
Output.pay_pubkey_hash(tx, 1, CENT, NULL_HASH)
|
||||
tx.add_inputs([cls.get_input()])
|
||||
return tx
|
||||
|
||||
@classmethod
|
||||
def get_claim_transaction(cls, claim_name, claim=''):
|
||||
tx = Transaction()
|
||||
Output.pay_claim_name_pubkey_hash(tx, 1, CENT, claim_name, claim, NULL_HASH)
|
||||
tx.add_inputs([cls.get_input()])
|
||||
return tx
|
||||
|
||||
def test_output_size_and_fee(self):
|
||||
txo = self.get_output()
|
||||
self.assertEqual(txo.size, 46)
|
||||
self.assertEqual(txo.fee, 46 * FEE_PER_BYTE)
|
||||
|
||||
def test_input_size_and_fee(self):
|
||||
txi = self.get_input()
|
||||
self.assertEqual(txi.size, 148)
|
||||
self.assertEqual(txi.fee, 148 * FEE_PER_BYTE)
|
||||
|
||||
def test_transaction_size_and_fee(self):
|
||||
tx = self.get_transaction()
|
||||
base_size = tx.size - 1 - tx.inputs[0].size
|
||||
self.assertEqual(tx.size, 204)
|
||||
self.assertEqual(tx.base_size, base_size)
|
||||
self.assertEqual(tx.base_fee, FEE_PER_BYTE * base_size)
|
||||
|
||||
def test_claim_name_transaction_size_and_fee(self):
|
||||
# fee based on claim name is the larger fee
|
||||
claim_name = 'verylongname'
|
||||
tx = self.get_claim_transaction(claim_name, '0'*4000)
|
||||
base_size = tx.size - 1 - tx.inputs[0].size
|
||||
self.assertEqual(tx.size, 4225)
|
||||
self.assertEqual(tx.base_size, base_size)
|
||||
self.assertEqual(tx.base_fee, len(claim_name) * FEE_PER_CHAR)
|
||||
# fee based on total bytes is the larger fee
|
||||
claim_name = 'a'
|
||||
tx = self.get_claim_transaction(claim_name, '0'*4000)
|
||||
base_size = tx.size - 1 - tx.inputs[0].size
|
||||
self.assertEqual(tx.size, 4214)
|
||||
self.assertEqual(tx.base_size, base_size)
|
||||
self.assertEqual(tx.base_fee, FEE_PER_BYTE * base_size)
|
||||
|
||||
|
||||
class TestTransactionSerialization(unittest.TestCase):
|
||||
|
||||
def test_genesis_transaction(self):
|
||||
raw = unhexlify(
|
||||
"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1f0"
|
||||
"4ffff001d010417696e736572742074696d657374616d7020737472696e67ffffffff01000004bfc91b8e"
|
||||
"001976a914345991dbf57bfb014b87006acdfafbfc5fe8292f88ac00000000"
|
||||
)
|
||||
tx = Transaction(raw)
|
||||
self.assertEqual(tx.version, 1)
|
||||
self.assertEqual(tx.locktime, 0)
|
||||
self.assertEqual(len(tx.inputs), 1)
|
||||
self.assertEqual(len(tx.outputs), 1)
|
||||
|
||||
coinbase = tx.inputs[0]
|
||||
self.assertEqual(coinbase.output_tx_hash, NULL_HASH)
|
||||
self.assertEqual(coinbase.output_index, 0xFFFFFFFF)
|
||||
self.assertEqual(coinbase.sequence, 0xFFFFFFFF)
|
||||
self.assertTrue(coinbase.is_coinbase)
|
||||
self.assertEqual(coinbase.script, None)
|
||||
self.assertEqual(
|
||||
hexlify(coinbase.coinbase),
|
||||
b'04ffff001d010417696e736572742074696d657374616d7020737472696e67'
|
||||
)
|
||||
|
||||
out = tx.outputs[0]
|
||||
self.assertEqual(out.amount, 40000000000000000)
|
||||
self.assertEqual(out.index, 0)
|
||||
self.assertTrue(out.script.is_pay_pubkey_hash)
|
||||
self.assertFalse(out.script.is_pay_script_hash)
|
||||
self.assertFalse(out.script.is_claim_involved)
|
||||
|
||||
tx._reset()
|
||||
self.assertEqual(tx.raw, raw)
|
||||
|
||||
def test_coinbase_transaction(self):
|
||||
raw = unhexlify(
|
||||
"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff200"
|
||||
"34d520504f89ac55a086032d217bf0700000d2f6e6f64655374726174756d2f0000000001a03489850800"
|
||||
"00001976a914cfab870d6deea54ca94a41912a75484649e52f2088ac00000000"
|
||||
)
|
||||
tx = Transaction(raw)
|
||||
self.assertEqual(tx.version, 1)
|
||||
self.assertEqual(tx.locktime, 0)
|
||||
self.assertEqual(len(tx.inputs), 1)
|
||||
self.assertEqual(len(tx.outputs), 1)
|
||||
|
||||
coinbase = tx.inputs[0]
|
||||
self.assertEqual(coinbase.output_tx_hash, NULL_HASH)
|
||||
self.assertEqual(coinbase.output_index, 0xFFFFFFFF)
|
||||
self.assertEqual(coinbase.sequence, 0)
|
||||
self.assertTrue(coinbase.is_coinbase)
|
||||
self.assertEqual(coinbase.script, None)
|
||||
self.assertEqual(
|
||||
hexlify(coinbase.coinbase),
|
||||
b'034d520504f89ac55a086032d217bf0700000d2f6e6f64655374726174756d2f'
|
||||
)
|
||||
|
||||
out = tx.outputs[0]
|
||||
self.assertEqual(out.amount, 36600100000)
|
||||
self.assertEqual(out.index, 0)
|
||||
self.assertTrue(out.script.is_pay_pubkey_hash)
|
||||
self.assertFalse(out.script.is_pay_script_hash)
|
||||
self.assertFalse(out.script.is_claim_involved)
|
||||
|
||||
tx._reset()
|
||||
self.assertEqual(tx.raw, raw)
|
||||
|
||||
def test_claim_transaction(self):
|
||||
raw = unhexlify(
|
||||
"01000000012433e1b327603843b083344dbae5306ff7927f87ebbc5ae9eb50856c5b53fd1d000000006a4"
|
||||
"7304402201a91e1023d11c383a11e26bf8f9034087b15d8ada78fa565e0610455ffc8505e0220038a63a6"
|
||||
"ecb399723d4f1f78a20ddec0a78bf8fb6c75e63e166ef780f3944fbf0121021810150a2e4b088ec51b20c"
|
||||
"be1b335962b634545860733367824d5dc3eda767dffffffff028096980000000000fdff00b50463617473"
|
||||
"4cdc080110011a7808011230080410011a084d616361726f6e6922002a003214416c6c207269676874732"
|
||||
"072657365727665642e38004a0052005a001a42080110011a30add80aaf02559ba09853636a0658c42b72"
|
||||
"7cb5bb4ba8acedb4b7fe656065a47a31878dbf9912135ddb9e13806cc1479d220a696d6167652f6a70656"
|
||||
"72a5c080110031a404180cc0fa4d3839ee29cca866baed25fafb43fca1eb3b608ee889d351d3573d042c7"
|
||||
"b83e2e643db0d8e062a04e6e9ae6b90540a2f95fe28638d0f18af4361a1c2214f73de93f4299fb32c32f9"
|
||||
"49e02198a8e91101abd6d7576a914be16e4b0f9bd8f6d47d02b3a887049c36d3b84cb88ac0cd2520b0000"
|
||||
"00001976a914f521178feb733a719964e1da4a9efb09dcc39cfa88ac00000000"
|
||||
)
|
||||
tx = Transaction(raw)
|
||||
self.assertEqual(hexlify(tx.id), b'666c3d15de1d6949a4fe717126c368e274b36957dce29fd401138c1e87e92a62')
|
||||
self.assertEqual(tx.version, 1)
|
||||
self.assertEqual(tx.locktime, 0)
|
||||
self.assertEqual(len(tx.inputs), 1)
|
||||
self.assertEqual(len(tx.outputs), 2)
|
||||
|
||||
txin = tx.inputs[0] # type: Input
|
||||
self.assertEqual(
|
||||
hexlify(txin.output_tx_hash[::-1]),
|
||||
b'1dfd535b6c8550ebe95abceb877f92f76f30e5ba4d3483b043386027b3e13324'
|
||||
)
|
||||
self.assertEqual(txin.output_index, 0)
|
||||
self.assertEqual(txin.sequence, 0xFFFFFFFF)
|
||||
self.assertFalse(txin.is_coinbase)
|
||||
self.assertEqual(txin.script.template.name, 'pubkey_hash')
|
||||
self.assertEqual(
|
||||
hexlify(txin.script.values['pubkey']),
|
||||
b'021810150a2e4b088ec51b20cbe1b335962b634545860733367824d5dc3eda767d'
|
||||
)
|
||||
self.assertEqual(
|
||||
hexlify(txin.script.values['signature']),
|
||||
b'304402201a91e1023d11c383a11e26bf8f9034087b15d8ada78fa565e0610455ffc8505e0220038a63a6'
|
||||
b'ecb399723d4f1f78a20ddec0a78bf8fb6c75e63e166ef780f3944fbf01'
|
||||
)
|
||||
|
||||
# Claim
|
||||
out0 = tx.outputs[0] # type: Output
|
||||
self.assertEqual(out0.amount, 10000000)
|
||||
self.assertEqual(out0.index, 0)
|
||||
self.assertTrue(out0.script.is_pay_pubkey_hash)
|
||||
self.assertTrue(out0.script.is_claim_name)
|
||||
self.assertTrue(out0.script.is_claim_involved)
|
||||
self.assertEqual(out0.script.values['claim_name'], b'cats')
|
||||
self.assertEqual(
|
||||
hexlify(out0.script.values['pubkey_hash']),
|
||||
b'be16e4b0f9bd8f6d47d02b3a887049c36d3b84cb'
|
||||
)
|
||||
|
||||
# Change
|
||||
out1 = tx.outputs[1] # type: Output
|
||||
self.assertEqual(out1.amount, 189977100)
|
||||
self.assertEqual(out1.index, 1)
|
||||
self.assertTrue(out1.script.is_pay_pubkey_hash)
|
||||
self.assertFalse(out1.script.is_claim_involved)
|
||||
self.assertEqual(
|
||||
hexlify(out1.script.values['pubkey_hash']),
|
||||
b'f521178feb733a719964e1da4a9efb09dcc39cfa'
|
||||
)
|
||||
|
||||
tx._reset()
|
||||
self.assertEqual(tx.raw, raw)
|
||||
|
||||
|
||||
class TestTransactionSigning(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.private_key = PrivateKey.from_seed(Mnemonic.mnemonic_to_seed(
|
||||
'program leader library giant team normal suspect crater pair miracle sweet until absent'
|
||||
))
|
||||
|
||||
def test_sign(self):
|
||||
tx = Transaction()
|
||||
Output.pay_pubkey_hash(Transaction(), 0, CENT, NULL_HASH).spend(fake=True)
|
||||
tx.add_inputs([self.get_input()])
|
||||
Output.pay_pubkey_hash(tx, 0, CENT, NULL_HASH)
|
||||
tx = self.get_tx()
|
||||
|
98
lbrynet/tests/unit/wallet/test_wallet.py
Normal file
98
lbrynet/tests/unit/wallet/test_wallet.py
Normal file
|
@ -0,0 +1,98 @@
|
|||
from twisted.trial import unittest
|
||||
from lbrynet.wallet.wallet import Account, Wallet
|
||||
from lbrynet.wallet.manager import WalletManager
|
||||
from lbrynet.wallet import set_wallet_manager
|
||||
|
||||
|
||||
class TestWalletAccount(unittest.TestCase):
|
||||
|
||||
def test_wallet_automatically_creates_default_account(self):
|
||||
wallet = Wallet()
|
||||
set_wallet_manager(WalletManager(wallet=wallet))
|
||||
account = wallet.default_account # type: Account
|
||||
self.assertIsInstance(account, Account)
|
||||
self.assertEqual(len(account.receiving_keys.child_keys), 0)
|
||||
self.assertEqual(len(account.receiving_keys.addresses), 0)
|
||||
self.assertEqual(len(account.change_keys.child_keys), 0)
|
||||
self.assertEqual(len(account.change_keys.addresses), 0)
|
||||
wallet.ensure_enough_addresses()
|
||||
self.assertEqual(len(account.receiving_keys.child_keys), 20)
|
||||
self.assertEqual(len(account.receiving_keys.addresses), 20)
|
||||
self.assertEqual(len(account.change_keys.child_keys), 6)
|
||||
self.assertEqual(len(account.change_keys.addresses), 6)
|
||||
|
||||
def test_generate_account_from_seed(self):
|
||||
account = Account.generate_from_seed(
|
||||
"carbon smart garage balance margin twelve chest sword toast envelope bottom stomach ab"
|
||||
"sent"
|
||||
) # type: Account
|
||||
self.assertEqual(
|
||||
account.private_key.extended_key_string(),
|
||||
"xprv9s21ZrQH143K42ovpZygnjfHdAqSd9jo7zceDfPRogM7bkkoNVv7DRNLEoB8HoirMgH969NrgL8jNzLEeg"
|
||||
"qFzPRWM37GXd4uE8uuRkx4LAe",
|
||||
)
|
||||
self.assertEqual(
|
||||
account.public_key.extended_key_string(),
|
||||
"xpub661MyMwAqRbcGWtPvbWh9sc2BCfw2cTeVDYF23o3N1t6UZ5wv3EMmDgp66FxHuDtWdft3B5eL5xQtyzAtk"
|
||||
"dmhhC95gjRjLzSTdkho95asu9",
|
||||
)
|
||||
self.assertEqual(
|
||||
account.receiving_keys.generate_next_address(),
|
||||
'bCqJrLHdoiRqEZ1whFZ3WHNb33bP34SuGx'
|
||||
)
|
||||
private_key = account.get_private_key_for_address('bCqJrLHdoiRqEZ1whFZ3WHNb33bP34SuGx')
|
||||
self.assertEqual(
|
||||
private_key.extended_key_string(),
|
||||
'xprv9vwXVierUTT4hmoe3dtTeBfbNv1ph2mm8RWXARU6HsZjBaAoFaS2FRQu4fptRAyJWhJW42dmsEaC1nKnVK'
|
||||
'KTMhq3TVEHsNj1ca3ciZMKktT'
|
||||
)
|
||||
self.assertIsNone(account.get_private_key_for_address('BcQjRlhDOIrQez1WHfz3whnB33Bp34sUgX'))
|
||||
|
||||
def test_load_and_save_account(self):
|
||||
wallet_data = {
|
||||
'name': 'Main Wallet',
|
||||
'accounts': {
|
||||
0: {
|
||||
'seed':
|
||||
"carbon smart garage balance margin twelve chest sword toast envelope botto"
|
||||
"m stomach absent",
|
||||
'encrypted': False,
|
||||
'private_key':
|
||||
"xprv9s21ZrQH143K42ovpZygnjfHdAqSd9jo7zceDfPRogM7bkkoNVv7DRNLEoB8HoirMgH969"
|
||||
"NrgL8jNzLEegqFzPRWM37GXd4uE8uuRkx4LAe",
|
||||
'public_key':
|
||||
"xpub661MyMwAqRbcGWtPvbWh9sc2BCfw2cTeVDYF23o3N1t6UZ5wv3EMmDgp66FxHuDtWdft3B"
|
||||
"5eL5xQtyzAtkdmhhC95gjRjLzSTdkho95asu9",
|
||||
'receiving_gap': 10,
|
||||
'receiving_keys': [
|
||||
'02c68e2d1cf85404c86244ffa279f4c5cd00331e996d30a86d6e46480e3a9220f4',
|
||||
'03c5a997d0549875d23b8e4bbc7b4d316d962587483f3a2e62ddd90a21043c4941'],
|
||||
'change_gap': 10,
|
||||
'change_keys': [
|
||||
'021460e8d728eee325d0d43128572b2e2bacdc027e420451df100cf9f2154ea5ab']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wallet = Wallet.from_json(wallet_data)
|
||||
set_wallet_manager(WalletManager(wallet=wallet))
|
||||
self.assertEqual(wallet.name, 'Main Wallet')
|
||||
|
||||
account = wallet.default_account
|
||||
self.assertIsInstance(account, Account)
|
||||
|
||||
self.assertEqual(len(account.receiving_keys.addresses), 2)
|
||||
self.assertEqual(
|
||||
account.receiving_keys.addresses[0],
|
||||
'bCqJrLHdoiRqEZ1whFZ3WHNb33bP34SuGx'
|
||||
)
|
||||
self.assertEqual(len(account.change_keys.addresses), 1)
|
||||
self.assertEqual(
|
||||
account.change_keys.addresses[0],
|
||||
'bFpHENtqugKKHDshKFq2Mnb59Y2bx4vKgL'
|
||||
)
|
||||
|
||||
self.assertDictEqual(
|
||||
wallet_data['accounts'][0],
|
||||
account.to_json()
|
||||
)
|
|
@ -0,0 +1,10 @@
|
|||
_wallet_manager = None
|
||||
|
||||
|
||||
def set_wallet_manager(wallet_manager):
|
||||
global _wallet_manager
|
||||
_wallet_manager = wallet_manager
|
||||
|
||||
|
||||
def get_wallet_manager():
|
||||
return _wallet_manager
|
|
@ -1,75 +1,141 @@
|
|||
import logging
|
||||
from binascii import hexlify, unhexlify
|
||||
from itertools import chain
|
||||
from lbrynet.wallet import get_wallet_manager
|
||||
from lbrynet.wallet.mnemonic import Mnemonic
|
||||
from lbrynet.wallet.bip32 import PrivateKey, PubKey, from_extended_key_string
|
||||
from lbrynet.wallet.hash import double_sha256, aes_encrypt, aes_decrypt
|
||||
|
||||
from lbryschema.address import public_key_to_address
|
||||
|
||||
from .lbrycrd import deserialize_xkey
|
||||
from .lbrycrd import CKD_pub
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
class KeyChain:
|
||||
|
||||
|
||||
def get_key_chain_from_xpub(xpub):
|
||||
_, _, _, chain, key = deserialize_xkey(xpub)
|
||||
return key, chain
|
||||
|
||||
|
||||
class AddressSequence:
|
||||
|
||||
def __init__(self, derived_keys, gap, age_checker, pub_key, chain_key):
|
||||
self.gap = gap
|
||||
self.is_old = age_checker
|
||||
self.pub_key = pub_key
|
||||
self.chain_key = chain_key
|
||||
self.derived_keys = derived_keys
|
||||
def __init__(self, parent_key, child_keys, gap):
|
||||
self.parent_key = parent_key # type: PubKey
|
||||
self.child_keys = child_keys
|
||||
self.minimum_gap = gap
|
||||
self.addresses = [
|
||||
public_key_to_address(key.decode('hex'))
|
||||
for key in derived_keys
|
||||
public_key_to_address(key)
|
||||
for key in child_keys
|
||||
]
|
||||
|
||||
def generate_next_address(self):
|
||||
new_key, _ = CKD_pub(self.pub_key, self.chain_key, len(self.derived_keys))
|
||||
address = public_key_to_address(new_key)
|
||||
self.derived_keys.append(new_key.encode('hex'))
|
||||
self.addresses.append(address)
|
||||
return address
|
||||
|
||||
@property
|
||||
def has_gap(self):
|
||||
if len(self.addresses) < self.gap:
|
||||
if len(self.addresses) < self.minimum_gap:
|
||||
return False
|
||||
for address in self.addresses[-self.gap:]:
|
||||
if self.is_old(address):
|
||||
ledger = get_wallet_manager().ledger
|
||||
for address in self.addresses[-self.minimum_gap:]:
|
||||
if ledger.is_address_old(address):
|
||||
return False
|
||||
return True
|
||||
|
||||
def generate_next_address(self):
|
||||
child_key = self.parent_key.child(len(self.child_keys))
|
||||
self.child_keys.append(child_key.pubkey_bytes)
|
||||
self.addresses.append(child_key.address)
|
||||
return child_key.address
|
||||
|
||||
def ensure_enough_addresses(self):
|
||||
starting_length = len(self.addresses)
|
||||
while not self.has_gap():
|
||||
while not self.has_gap:
|
||||
self.generate_next_address()
|
||||
return self.addresses[starting_length:]
|
||||
|
||||
|
||||
class Account:
|
||||
|
||||
def __init__(self, data, receiving_gap, change_gap, age_checker):
|
||||
self.xpub = data['xpub']
|
||||
master_key, master_chain = get_key_chain_from_xpub(data['xpub'])
|
||||
self.receiving = AddressSequence(
|
||||
data.get('receiving', []), receiving_gap, age_checker,
|
||||
*CKD_pub(master_key, master_chain, 0)
|
||||
)
|
||||
self.change = AddressSequence(
|
||||
data.get('change', []), change_gap, age_checker,
|
||||
*CKD_pub(master_key, master_chain, 1)
|
||||
)
|
||||
self.is_old = age_checker
|
||||
def __init__(self, seed, encrypted, private_key, public_key, **kwargs):
|
||||
self.seed = seed
|
||||
self.encrypted = encrypted
|
||||
self.private_key = private_key # type: PrivateKey
|
||||
self.public_key = public_key # type: PubKey
|
||||
self.receiving_gap = kwargs.get('receiving_gap', 20)
|
||||
self.receiving_keys = kwargs.get('receiving_keys') or \
|
||||
KeyChain(self.public_key.child(0), [], self.receiving_gap)
|
||||
self.change_gap = kwargs.get('change_gap', 6)
|
||||
self.change_keys = kwargs.get('change_keys') or \
|
||||
KeyChain(self.public_key.child(1), [], self.change_gap)
|
||||
self.keychains = [
|
||||
self.receiving_keys, # child: 0
|
||||
self.change_keys # child: 1
|
||||
]
|
||||
|
||||
def as_dict(self):
|
||||
@classmethod
|
||||
def generate(cls):
|
||||
seed = Mnemonic().make_seed()
|
||||
return cls.generate_from_seed(seed)
|
||||
|
||||
@classmethod
|
||||
def generate_from_seed(cls, seed):
|
||||
private_key = cls.get_private_key_from_seed(seed)
|
||||
return cls(
|
||||
seed=seed, encrypted=False,
|
||||
private_key=private_key,
|
||||
public_key=private_key.public_key,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data):
|
||||
data = json_data.copy()
|
||||
if not data['encrypted']:
|
||||
data['private_key'] = from_extended_key_string(data['private_key'])
|
||||
data['public_key'] = from_extended_key_string(data['public_key'])
|
||||
data['receiving_keys'] = KeyChain(
|
||||
data['public_key'].child(0),
|
||||
[unhexlify(k) for k in data['receiving_keys']],
|
||||
data['receiving_gap']
|
||||
)
|
||||
data['change_keys'] = KeyChain(
|
||||
data['public_key'].child(1),
|
||||
[unhexlify(k) for k in data['change_keys']],
|
||||
data['change_gap']
|
||||
)
|
||||
return cls(**data)
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
'receiving': self.receiving.derived_keys,
|
||||
'change': self.change.derived_keys,
|
||||
'xpub': self.xpub
|
||||
'seed': self.seed,
|
||||
'encrypted': self.encrypted,
|
||||
'private_key': self.private_key.extended_key_string(),
|
||||
'public_key': self.public_key.extended_key_string(),
|
||||
'receiving_keys': [hexlify(k) for k in self.receiving_keys.child_keys],
|
||||
'receiving_gap': self.receiving_gap,
|
||||
'change_keys': [hexlify(k) for k in self.change_keys.child_keys],
|
||||
'change_gap': self.change_gap
|
||||
}
|
||||
|
||||
def decrypt(self, password):
|
||||
assert self.encrypted, "Key is not encrypted."
|
||||
secret = double_sha256(password)
|
||||
self.seed = aes_decrypt(secret, self.seed)
|
||||
self.private_key = from_extended_key_string(aes_decrypt(secret, self.private_key))
|
||||
self.encrypted = False
|
||||
|
||||
def encrypt(self, password):
|
||||
assert not self.encrypted, "Key is already encrypted."
|
||||
secret = double_sha256(password)
|
||||
self.seed = aes_encrypt(secret, self.seed)
|
||||
self.private_key = aes_encrypt(secret, self.private_key.extended_key_string())
|
||||
self.encrypted = True
|
||||
|
||||
@staticmethod
|
||||
def get_private_key_from_seed(seed):
|
||||
return PrivateKey.from_seed(Mnemonic.mnemonic_to_seed(seed))
|
||||
|
||||
@property
|
||||
def sequences(self):
|
||||
return self.receiving, self.change
|
||||
def addresses(self):
|
||||
return chain(self.receiving_keys.addresses, self.change_keys.addresses)
|
||||
|
||||
def get_private_key_for_address(self, address):
|
||||
assert not self.encrypted, "Cannot get private key on encrypted wallet account."
|
||||
for a, keychain in enumerate(self.keychains):
|
||||
for b, match in enumerate(keychain.addresses):
|
||||
if address == match:
|
||||
return self.private_key.child(a).child(b)
|
||||
|
||||
def ensure_enough_addresses(self):
|
||||
return [
|
||||
address
|
||||
for keychain in self.keychains
|
||||
for address in keychain.ensure_enough_addresses()
|
||||
]
|
||||
|
|
320
lbrynet/wallet/bip32.py
Normal file
320
lbrynet/wallet/bip32.py
Normal file
|
@ -0,0 +1,320 @@
|
|||
# Copyright (c) 2017, Neil Booth
|
||||
# Copyright (c) 2018, LBRY Inc.
|
||||
#
|
||||
# All rights reserved.
|
||||
#
|
||||
# See the file "LICENCE" for information about the copyright
|
||||
# and warranty status of this software.
|
||||
|
||||
""" Logic for BIP32 Hierarchical Key Derivation. """
|
||||
|
||||
import struct
|
||||
import hashlib
|
||||
from binascii import unhexlify
|
||||
from six import int2byte, byte2int
|
||||
|
||||
import ecdsa
|
||||
import ecdsa.ellipticcurve as EC
|
||||
import ecdsa.numbertheory as NT
|
||||
|
||||
from .hash import Base58, hmac_sha512, hash160, double_sha256, public_key_to_address
|
||||
from .util import cachedproperty, bytes_to_int, int_to_bytes
|
||||
|
||||
|
||||
class DerivationError(Exception):
|
||||
""" Raised when an invalid derivation occurs. """
|
||||
|
||||
|
||||
class _KeyBase(object):
|
||||
""" A BIP32 Key, public or private. """
|
||||
|
||||
CURVE = ecdsa.SECP256k1
|
||||
|
||||
def __init__(self, chain_code, n, depth, parent):
|
||||
if not isinstance(chain_code, (bytes, bytearray)):
|
||||
raise TypeError('chain code must be raw bytes')
|
||||
if len(chain_code) != 32:
|
||||
raise ValueError('invalid chain code')
|
||||
if not 0 <= n < 1 << 32:
|
||||
raise ValueError('invalid child number')
|
||||
if not 0 <= depth < 256:
|
||||
raise ValueError('invalid depth')
|
||||
if parent is not None:
|
||||
if not isinstance(parent, type(self)):
|
||||
raise TypeError('parent key has bad type')
|
||||
self.chain_code = chain_code
|
||||
self.n = n
|
||||
self.depth = depth
|
||||
self.parent = parent
|
||||
|
||||
def _hmac_sha512(self, msg):
|
||||
""" Use SHA-512 to provide an HMAC, returned as a pair of 32-byte objects. """
|
||||
hmac = hmac_sha512(self.chain_code, msg)
|
||||
return hmac[:32], hmac[32:]
|
||||
|
||||
def _extended_key(self, ver_bytes, raw_serkey):
|
||||
""" Return the 78-byte extended key given prefix version bytes and serialized key bytes. """
|
||||
if not isinstance(ver_bytes, (bytes, bytearray)):
|
||||
raise TypeError('ver_bytes must be raw bytes')
|
||||
if len(ver_bytes) != 4:
|
||||
raise ValueError('ver_bytes must have length 4')
|
||||
if not isinstance(raw_serkey, (bytes, bytearray)):
|
||||
raise TypeError('raw_serkey must be raw bytes')
|
||||
if len(raw_serkey) != 33:
|
||||
raise ValueError('raw_serkey must have length 33')
|
||||
|
||||
return (ver_bytes + int2byte(self.depth)
|
||||
+ self.parent_fingerprint() + struct.pack('>I', self.n)
|
||||
+ self.chain_code + raw_serkey)
|
||||
|
||||
def fingerprint(self):
|
||||
""" Return the key's fingerprint as 4 bytes. """
|
||||
return self.identifier()[:4]
|
||||
|
||||
def parent_fingerprint(self):
|
||||
""" Return the parent key's fingerprint as 4 bytes. """
|
||||
return self.parent.fingerprint() if self.parent else int2byte(0)*4
|
||||
|
||||
def extended_key_string(self):
|
||||
""" Return an extended key as a base58 string. """
|
||||
return Base58.encode_check(self.extended_key())
|
||||
|
||||
|
||||
class PubKey(_KeyBase):
|
||||
""" A BIP32 public key. """
|
||||
|
||||
def __init__(self, pubkey, chain_code, n, depth, parent=None):
|
||||
super(PubKey, self).__init__(chain_code, n, depth, parent)
|
||||
if isinstance(pubkey, ecdsa.VerifyingKey):
|
||||
self.verifying_key = pubkey
|
||||
else:
|
||||
self.verifying_key = self._verifying_key_from_pubkey(pubkey)
|
||||
|
||||
@classmethod
|
||||
def _verifying_key_from_pubkey(cls, pubkey):
|
||||
""" Converts a 33-byte compressed pubkey into an ecdsa.VerifyingKey object. """
|
||||
if not isinstance(pubkey, (bytes, bytearray)):
|
||||
raise TypeError('pubkey must be raw bytes')
|
||||
if len(pubkey) != 33:
|
||||
raise ValueError('pubkey must be 33 bytes')
|
||||
if byte2int(pubkey[0]) not in (2, 3):
|
||||
raise ValueError('invalid pubkey prefix byte')
|
||||
curve = cls.CURVE.curve
|
||||
|
||||
is_odd = byte2int(pubkey[0]) == 3
|
||||
x = bytes_to_int(pubkey[1:])
|
||||
|
||||
# p is the finite field order
|
||||
a, b, p = curve.a(), curve.b(), curve.p()
|
||||
y2 = pow(x, 3, p) + b
|
||||
assert a == 0 # Otherwise y2 += a * pow(x, 2, p)
|
||||
y = NT.square_root_mod_prime(y2 % p, p)
|
||||
if bool(y & 1) != is_odd:
|
||||
y = p - y
|
||||
point = EC.Point(curve, x, y)
|
||||
|
||||
return ecdsa.VerifyingKey.from_public_point(point, curve=cls.CURVE)
|
||||
|
||||
@cachedproperty
|
||||
def pubkey_bytes(self):
|
||||
""" Return the compressed public key as 33 bytes. """
|
||||
point = self.verifying_key.pubkey.point
|
||||
prefix = int2byte(2 + (point.y() & 1))
|
||||
padded_bytes = _exponent_to_bytes(point.x())
|
||||
return prefix + padded_bytes
|
||||
|
||||
@cachedproperty
|
||||
def address(self):
|
||||
""" The public key as a P2PKH address. """
|
||||
return public_key_to_address(self.pubkey_bytes, 'regtest')
|
||||
|
||||
def ec_point(self):
|
||||
return self.verifying_key.pubkey.point
|
||||
|
||||
def child(self, n):
|
||||
""" Return the derived child extended pubkey at index N. """
|
||||
if not 0 <= n < (1 << 31):
|
||||
raise ValueError('invalid BIP32 public key child number')
|
||||
|
||||
msg = self.pubkey_bytes + struct.pack('>I', n)
|
||||
L, R = self._hmac_sha512(msg)
|
||||
|
||||
curve = self.CURVE
|
||||
L = bytes_to_int(L)
|
||||
if L >= curve.order:
|
||||
raise DerivationError
|
||||
|
||||
point = curve.generator * L + self.ec_point()
|
||||
if point == EC.INFINITY:
|
||||
raise DerivationError
|
||||
|
||||
verkey = ecdsa.VerifyingKey.from_public_point(point, curve=curve)
|
||||
|
||||
return PubKey(verkey, R, n, self.depth + 1, self)
|
||||
|
||||
def identifier(self):
|
||||
""" Return the key's identifier as 20 bytes. """
|
||||
return hash160(self.pubkey_bytes)
|
||||
|
||||
def extended_key(self):
|
||||
""" Return a raw extended public key. """
|
||||
return self._extended_key(unhexlify("0488b21e"), self.pubkey_bytes)
|
||||
|
||||
|
||||
class LowSValueSigningKey(ecdsa.SigningKey):
|
||||
"""
|
||||
Enforce low S values in signatures
|
||||
BIP-0062: https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#low-s-values-in-signatures
|
||||
"""
|
||||
|
||||
def sign_number(self, number, entropy=None, k=None):
|
||||
order = self.privkey.order
|
||||
r, s = ecdsa.SigningKey.sign_number(self, number, entropy, k)
|
||||
if s > order / 2:
|
||||
s = order - s
|
||||
return r, s
|
||||
|
||||
|
||||
class PrivateKey(_KeyBase):
|
||||
"""A BIP32 private key."""
|
||||
|
||||
HARDENED = 1 << 31
|
||||
|
||||
def __init__(self, privkey, chain_code, n, depth, parent=None):
|
||||
super(PrivateKey, self).__init__(chain_code, n, depth, parent)
|
||||
if isinstance(privkey, ecdsa.SigningKey):
|
||||
self.signing_key = privkey
|
||||
else:
|
||||
self.signing_key = self._signing_key_from_privkey(privkey)
|
||||
|
||||
@classmethod
|
||||
def _signing_key_from_privkey(cls, private_key):
|
||||
""" Converts a 32-byte private key into an ecdsa.SigningKey object. """
|
||||
exponent = cls._private_key_secret_exponent(private_key)
|
||||
return LowSValueSigningKey.from_secret_exponent(exponent, curve=cls.CURVE)
|
||||
|
||||
@classmethod
|
||||
def _private_key_secret_exponent(cls, private_key):
|
||||
""" Return the private key as a secret exponent if it is a valid private key. """
|
||||
if not isinstance(private_key, (bytes, bytearray)):
|
||||
raise TypeError('private key must be raw bytes')
|
||||
if len(private_key) != 32:
|
||||
raise ValueError('private key must be 32 bytes')
|
||||
exponent = bytes_to_int(private_key)
|
||||
if not 1 <= exponent < cls.CURVE.order:
|
||||
raise ValueError('private key represents an invalid exponent')
|
||||
return exponent
|
||||
|
||||
@classmethod
|
||||
def from_seed(cls, seed):
|
||||
# This hard-coded message string seems to be coin-independent...
|
||||
hmac = hmac_sha512(b'Bitcoin seed', seed)
|
||||
privkey, chain_code = hmac[:32], hmac[32:]
|
||||
return cls(privkey, chain_code, 0, 0)
|
||||
|
||||
@cachedproperty
|
||||
def private_key_bytes(self):
|
||||
""" Return the serialized private key (no leading zero byte). """
|
||||
return _exponent_to_bytes(self.secret_exponent())
|
||||
|
||||
@cachedproperty
|
||||
def public_key(self):
|
||||
""" Return the corresponding extended public key. """
|
||||
verifying_key = self.signing_key.get_verifying_key()
|
||||
parent_pubkey = self.parent.public_key if self.parent else None
|
||||
return PubKey(verifying_key, self.chain_code, self.n, self.depth,
|
||||
parent_pubkey)
|
||||
|
||||
def ec_point(self):
|
||||
return self.public_key.ec_point()
|
||||
|
||||
def secret_exponent(self):
|
||||
""" Return the private key as a secret exponent. """
|
||||
return self.signing_key.privkey.secret_multiplier
|
||||
|
||||
def wif(self):
|
||||
""" Return the private key encoded in Wallet Import Format. """
|
||||
return b'\x1c' + self.private_key_bytes + b'\x01'
|
||||
|
||||
def address(self):
|
||||
""" The public key as a P2PKH address. """
|
||||
return self.public_key.address
|
||||
|
||||
def child(self, n):
|
||||
""" Return the derived child extended private key at index N."""
|
||||
if not 0 <= n < (1 << 32):
|
||||
raise ValueError('invalid BIP32 private key child number')
|
||||
|
||||
if n >= self.HARDENED:
|
||||
serkey = b'\0' + self.private_key_bytes
|
||||
else:
|
||||
serkey = self.public_key.pubkey_bytes
|
||||
|
||||
msg = serkey + struct.pack('>I', n)
|
||||
L, R = self._hmac_sha512(msg)
|
||||
|
||||
curve = self.CURVE
|
||||
L = bytes_to_int(L)
|
||||
exponent = (L + bytes_to_int(self.private_key_bytes)) % curve.order
|
||||
if exponent == 0 or L >= curve.order:
|
||||
raise DerivationError
|
||||
|
||||
privkey = _exponent_to_bytes(exponent)
|
||||
|
||||
return PrivateKey(privkey, R, n, self.depth + 1, self)
|
||||
|
||||
def sign(self, data):
|
||||
""" Produce a signature for piece of data by double hashing it and signing the hash. """
|
||||
key = self.signing_key
|
||||
digest = double_sha256(data)
|
||||
return key.sign_digest_deterministic(digest, hashlib.sha256, ecdsa.util.sigencode_der)
|
||||
|
||||
def identifier(self):
|
||||
"""Return the key's identifier as 20 bytes."""
|
||||
return self.public_key.identifier()
|
||||
|
||||
def extended_key(self):
|
||||
"""Return a raw extended private key."""
|
||||
return self._extended_key(unhexlify("0488ade4"), b'\0' + self.private_key_bytes)
|
||||
|
||||
|
||||
def _exponent_to_bytes(exponent):
|
||||
"""Convert an exponent to 32 big-endian bytes"""
|
||||
return (int2byte(0)*32 + int_to_bytes(exponent))[-32:]
|
||||
|
||||
|
||||
def _from_extended_key(ekey):
|
||||
"""Return a PubKey or PrivateKey from an extended key raw bytes."""
|
||||
if not isinstance(ekey, (bytes, bytearray)):
|
||||
raise TypeError('extended key must be raw bytes')
|
||||
if len(ekey) != 78:
|
||||
raise ValueError('extended key must have length 78')
|
||||
|
||||
depth = byte2int(ekey[4])
|
||||
fingerprint = ekey[5:9] # Not used
|
||||
n, = struct.unpack('>I', ekey[9:13])
|
||||
chain_code = ekey[13:45]
|
||||
|
||||
if ekey[:4] == unhexlify("0488b21e"):
|
||||
pubkey = ekey[45:]
|
||||
key = PubKey(pubkey, chain_code, n, depth)
|
||||
elif ekey[:4] == unhexlify("0488ade4"):
|
||||
if ekey[45] is not int2byte(0):
|
||||
raise ValueError('invalid extended private key prefix byte')
|
||||
privkey = ekey[46:]
|
||||
key = PrivateKey(privkey, chain_code, n, depth)
|
||||
else:
|
||||
raise ValueError('version bytes unrecognised')
|
||||
|
||||
return key
|
||||
|
||||
|
||||
def from_extended_key_string(ekey_str):
|
||||
"""Given an extended key string, such as
|
||||
|
||||
xpub6BsnM1W2Y7qLMiuhi7f7dbAwQZ5Cz5gYJCRzTNainXzQXYjFwtuQXHd
|
||||
3qfi3t3KJtHxshXezfjft93w4UE7BGMtKwhqEHae3ZA7d823DVrL
|
||||
|
||||
return a PubKey or PrivateKey.
|
||||
"""
|
||||
return _from_extended_key(Base58.decode_check(ekey_str))
|
|
@ -19,22 +19,12 @@ class CoinSelector:
|
|||
debug and print([c.effective_amount for c in self.coins])
|
||||
|
||||
def select(self):
|
||||
if self.target > self.available:
|
||||
return
|
||||
if not self.coins:
|
||||
return
|
||||
if self.target > self.available:
|
||||
return
|
||||
return self.branch_and_bound() or self.single_random_draw()
|
||||
|
||||
def single_random_draw(self):
|
||||
self.random.shuffle(self.coins)
|
||||
selection = []
|
||||
amount = 0
|
||||
for coin in self.coins:
|
||||
selection.append(coin)
|
||||
amount += coin.effective_amount
|
||||
if amount >= self.target+self.cost_of_change:
|
||||
return selection
|
||||
|
||||
def branch_and_bound(self):
|
||||
# see bitcoin implementation for more info:
|
||||
# https://github.com/bitcoin/bitcoin/blob/master/src/wallet/coinselection.cpp
|
||||
|
@ -91,3 +81,13 @@ class CoinSelector:
|
|||
return [
|
||||
self.coins[i] for i, include in enumerate(best_selection) if include
|
||||
]
|
||||
|
||||
def single_random_draw(self):
|
||||
self.random.shuffle(self.coins)
|
||||
selection = []
|
||||
amount = 0
|
||||
for coin in self.coins:
|
||||
selection.append(coin)
|
||||
amount += coin.effective_amount
|
||||
if amount >= self.target+self.cost_of_change:
|
||||
return selection
|
||||
|
|
|
@ -9,9 +9,11 @@ SEED_PREFIX = '01' # Electrum standard wallet
|
|||
SEED_PREFIX_2FA = '101' # extended seed for two-factor authentication
|
||||
|
||||
|
||||
RECOMMENDED_FEE = 50000
|
||||
MAXIMUM_FEE_PER_BYTE = 50
|
||||
MAXIMUM_FEE_PER_NAME_CHAR = 200000
|
||||
COINBASE_MATURITY = 100
|
||||
COIN = 100000000
|
||||
CENT = 1000000
|
||||
COIN = 100*CENT
|
||||
|
||||
# supported types of transaction outputs
|
||||
TYPE_ADDRESS = 1
|
||||
|
@ -40,10 +42,13 @@ SERVER_RETRY_INTERVAL = 10
|
|||
MAX_BATCH_QUERY_SIZE = 500
|
||||
proxy_modes = ['socks4', 'socks5', 'http']
|
||||
|
||||
# Main network and testnet3 definitions
|
||||
# these values follow the parameters in lbrycrd/src/chainparams.cpp
|
||||
blockchain_params = {
|
||||
'lbrycrd_main': {
|
||||
# Chain Properties
|
||||
# see: https://github.com/lbryio/lbrycrd/blob/master/src/chainparams.cpp
|
||||
MAIN_CHAIN = 'main'
|
||||
TESTNET_CHAIN = 'testnet'
|
||||
REGTEST_CHAIN = 'regtest'
|
||||
CHAINS = {
|
||||
MAIN_CHAIN: {
|
||||
'pubkey_address': 0,
|
||||
'script_address': 5,
|
||||
'pubkey_address_prefix': 85,
|
||||
|
@ -53,7 +58,7 @@ blockchain_params = {
|
|||
'genesis_bits': 0x1f00ffff,
|
||||
'target_timespan': 150
|
||||
},
|
||||
'lbrycrd_testnet': {
|
||||
TESTNET_CHAIN: {
|
||||
'pubkey_address': 0,
|
||||
'script_address': 5,
|
||||
'pubkey_address_prefix': 111,
|
||||
|
@ -63,7 +68,7 @@ blockchain_params = {
|
|||
'genesis_bits': 0x1f00ffff,
|
||||
'target_timespan': 150
|
||||
},
|
||||
'lbrycrd_regtest': {
|
||||
REGTEST_CHAIN: {
|
||||
'pubkey_address': 0,
|
||||
'script_address': 5,
|
||||
'pubkey_address_prefix': 111,
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
import exceptions
|
||||
import types
|
||||
|
||||
|
||||
class EnumException(exceptions.Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Enumeration(object):
|
||||
"""
|
||||
enum-like type
|
||||
From the Python Cookbook, downloaded from http://code.activestate.com/recipes/67107/
|
||||
"""
|
||||
|
||||
def __init__(self, name, enumList):
|
||||
self.__doc__ = name
|
||||
lookup = {}
|
||||
reverseLookup = {}
|
||||
i = 0
|
||||
uniqueNames = []
|
||||
uniqueValues = []
|
||||
for x in enumList:
|
||||
if isinstance(x, types.TupleType):
|
||||
x, i = x
|
||||
if not isinstance(x, types.StringType):
|
||||
raise EnumException, "enum name is not a string: " + x
|
||||
if not isinstance(i, types.IntType):
|
||||
raise EnumException, "enum value is not an integer: " + i
|
||||
if x in uniqueNames:
|
||||
raise EnumException, "enum name is not unique: " + x
|
||||
if i in uniqueValues:
|
||||
raise EnumException, "enum value is not unique for " + x
|
||||
uniqueNames.append(x)
|
||||
uniqueValues.append(i)
|
||||
lookup[x] = i
|
||||
reverseLookup[i] = x
|
||||
i = i + 1
|
||||
self.lookup = lookup
|
||||
self.reverseLookup = reverseLookup
|
||||
|
||||
def __getattr__(self, attr):
|
||||
if attr not in self.lookup:
|
||||
raise AttributeError(attr)
|
||||
return self.lookup[attr]
|
||||
|
||||
def whatis(self, value):
|
||||
return self.reverseLookup[value]
|
183
lbrynet/wallet/hash.py
Normal file
183
lbrynet/wallet/hash.py
Normal file
|
@ -0,0 +1,183 @@
|
|||
# Copyright (c) 2016-2017, Neil Booth
|
||||
# Copyright (c) 2018, LBRY Inc.
|
||||
#
|
||||
# All rights reserved.
|
||||
#
|
||||
# See the file "LICENCE" for information about the copyright
|
||||
# and warranty status of this software.
|
||||
|
||||
""" Cryptography hash functions and related classes. """
|
||||
|
||||
import six
|
||||
import aes
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import struct
|
||||
from binascii import hexlify, unhexlify
|
||||
|
||||
from .util import bytes_to_int, int_to_bytes
|
||||
from .constants import CHAINS, MAIN_CHAIN
|
||||
|
||||
_sha256 = hashlib.sha256
|
||||
_sha512 = hashlib.sha512
|
||||
_new_hash = hashlib.new
|
||||
_new_hmac = hmac.new
|
||||
|
||||
|
||||
def sha256(x):
|
||||
""" Simple wrapper of hashlib sha256. """
|
||||
return _sha256(x).digest()
|
||||
|
||||
|
||||
def sha512(x):
|
||||
""" Simple wrapper of hashlib sha512. """
|
||||
return _sha512(x).digest()
|
||||
|
||||
|
||||
def ripemd160(x):
|
||||
""" Simple wrapper of hashlib ripemd160. """
|
||||
h = _new_hash('ripemd160')
|
||||
h.update(x)
|
||||
return h.digest()
|
||||
|
||||
|
||||
def pow_hash(x):
|
||||
r = sha512(double_sha256(x))
|
||||
r1 = ripemd160(r[:len(r) / 2])
|
||||
r2 = ripemd160(r[len(r) / 2:])
|
||||
r3 = double_sha256(r1 + r2)
|
||||
return r3
|
||||
|
||||
|
||||
def double_sha256(x):
|
||||
""" SHA-256 of SHA-256, as used extensively in bitcoin. """
|
||||
return sha256(sha256(x))
|
||||
|
||||
|
||||
def hmac_sha512(key, msg):
|
||||
""" Use SHA-512 to provide an HMAC. """
|
||||
return _new_hmac(key, msg, _sha512).digest()
|
||||
|
||||
|
||||
def hash160(x):
|
||||
""" RIPEMD-160 of SHA-256.
|
||||
Used to make bitcoin addresses from pubkeys. """
|
||||
return ripemd160(sha256(x))
|
||||
|
||||
|
||||
def hash_to_hex_str(x):
|
||||
""" Convert a big-endian binary hash to displayed hex string.
|
||||
Display form of a binary hash is reversed and converted to hex. """
|
||||
return hexlify(reversed(x))
|
||||
|
||||
|
||||
def hex_str_to_hash(x):
|
||||
""" Convert a displayed hex string to a binary hash. """
|
||||
return reversed(unhexlify(x))
|
||||
|
||||
|
||||
def public_key_to_address(public_key, chain=MAIN_CHAIN):
|
||||
return hash160_to_address(hash160(public_key), chain)
|
||||
|
||||
|
||||
def hash160_to_address(h160, chain=MAIN_CHAIN):
|
||||
prefix = CHAINS[chain]['pubkey_address_prefix']
|
||||
raw_address = six.int2byte(prefix) + h160
|
||||
return Base58.encode(raw_address + double_sha256(raw_address)[0:4])
|
||||
|
||||
|
||||
def address_to_hash_160(address):
|
||||
bytes = Base58.decode(address)
|
||||
prefix, pubkey_bytes, addr_checksum = bytes[0], bytes[1:21], bytes[21:]
|
||||
return pubkey_bytes
|
||||
|
||||
|
||||
def claim_id_hash(txid, n):
|
||||
return hash160(txid + struct.pack('>I', n))
|
||||
|
||||
|
||||
def aes_encrypt(secret, value):
|
||||
return base64.b64encode(aes.encryptData(secret, value.encode('utf8')))
|
||||
|
||||
|
||||
def aes_decrypt(secret, value):
|
||||
return aes.decryptData(secret, base64.b64decode(value)).decode('utf8')
|
||||
|
||||
|
||||
class Base58Error(Exception):
|
||||
""" Exception used for Base58 errors. """
|
||||
|
||||
|
||||
class Base58(object):
|
||||
""" Class providing base 58 functionality. """
|
||||
|
||||
chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
||||
assert len(chars) == 58
|
||||
cmap = {c: n for n, c in enumerate(chars)}
|
||||
|
||||
@staticmethod
|
||||
def char_value(c):
|
||||
val = Base58.cmap.get(c)
|
||||
if val is None:
|
||||
raise Base58Error('invalid base 58 character "{}"'.format(c))
|
||||
return val
|
||||
|
||||
@staticmethod
|
||||
def decode(txt):
|
||||
""" Decodes txt into a big-endian bytearray. """
|
||||
if not isinstance(txt, str):
|
||||
raise TypeError('a string is required')
|
||||
|
||||
if not txt:
|
||||
raise Base58Error('string cannot be empty')
|
||||
|
||||
value = 0
|
||||
for c in txt:
|
||||
value = value * 58 + Base58.char_value(c)
|
||||
|
||||
result = int_to_bytes(value)
|
||||
|
||||
# Prepend leading zero bytes if necessary
|
||||
count = 0
|
||||
for c in txt:
|
||||
if c != '1':
|
||||
break
|
||||
count += 1
|
||||
if count:
|
||||
result = six.int2byte(0)*count + result
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def encode(be_bytes):
|
||||
"""Converts a big-endian bytearray into a base58 string."""
|
||||
value = bytes_to_int(be_bytes)
|
||||
|
||||
txt = ''
|
||||
while value:
|
||||
value, mod = divmod(value, 58)
|
||||
txt += Base58.chars[mod]
|
||||
|
||||
for byte in be_bytes:
|
||||
if byte != 0:
|
||||
break
|
||||
txt += '1'
|
||||
|
||||
return txt[::-1]
|
||||
|
||||
@staticmethod
|
||||
def decode_check(txt, hash_fn=double_sha256):
|
||||
""" Decodes a Base58Check-encoded string to a payload. The version prefixes it. """
|
||||
be_bytes = Base58.decode(txt)
|
||||
result, check = be_bytes[:-4], be_bytes[-4:]
|
||||
if check != hash_fn(result)[:4]:
|
||||
raise Base58Error('invalid base 58 checksum for {}'.format(txt))
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def encode_check(payload, hash_fn=double_sha256):
|
||||
""" Encodes a payload bytearray (which includes the version byte(s))
|
||||
into a Base58Check string."""
|
||||
be_bytes = payload + hash_fn(payload)[:4]
|
||||
return Base58.encode(be_bytes)
|
|
@ -1,50 +0,0 @@
|
|||
import hashlib
|
||||
import hmac
|
||||
|
||||
|
||||
def sha256(x):
|
||||
return hashlib.sha256(x).digest()
|
||||
|
||||
|
||||
def sha512(x):
|
||||
return hashlib.sha512(x).digest()
|
||||
|
||||
|
||||
def ripemd160(x):
|
||||
h = hashlib.new('ripemd160')
|
||||
h.update(x)
|
||||
return h.digest()
|
||||
|
||||
|
||||
def Hash(x):
|
||||
if type(x) is unicode:
|
||||
x = x.encode('utf-8')
|
||||
return sha256(sha256(x))
|
||||
|
||||
|
||||
def PoWHash(x):
|
||||
if type(x) is unicode:
|
||||
x = x.encode('utf-8')
|
||||
r = sha512(Hash(x))
|
||||
r1 = ripemd160(r[:len(r) / 2])
|
||||
r2 = ripemd160(r[len(r) / 2:])
|
||||
r3 = Hash(r1 + r2)
|
||||
return r3
|
||||
|
||||
|
||||
def hash_encode(x):
|
||||
return x[::-1].encode('hex')
|
||||
|
||||
|
||||
def hash_decode(x):
|
||||
return x.decode('hex')[::-1]
|
||||
|
||||
|
||||
def hmac_sha_512(x, y):
|
||||
return hmac.new(x, y, hashlib.sha512).digest()
|
||||
|
||||
|
||||
def hash_160(public_key):
|
||||
md = hashlib.new('ripemd160')
|
||||
md.update(sha256(public_key))
|
||||
return md.digest()
|
|
@ -1,633 +0,0 @@
|
|||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import struct
|
||||
import logging
|
||||
import aes
|
||||
import ecdsa
|
||||
from ecdsa import numbertheory, util
|
||||
from ecdsa.curves import SECP256k1
|
||||
from ecdsa.ecdsa import curve_secp256k1, generator_secp256k1
|
||||
from ecdsa.ellipticcurve import Point
|
||||
from ecdsa.util import number_to_string, string_to_number
|
||||
|
||||
from lbryschema.address import public_key_to_address
|
||||
from lbryschema.schema import B58_CHARS
|
||||
from lbryschema.base import b58encode_with_checksum, b58decode_strip_checksum
|
||||
|
||||
from . import msqr
|
||||
from .util import rev_hex, var_int, int_to_hex
|
||||
from .hashing import Hash, sha256, hash_160
|
||||
from .errors import InvalidPassword, InvalidClaimId
|
||||
from .constants import CLAIM_ID_SIZE
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# AES encryption
|
||||
EncodeAES = lambda secret, s: base64.b64encode(aes.encryptData(secret, s))
|
||||
DecodeAES = lambda secret, e: aes.decryptData(secret, base64.b64decode(e))
|
||||
|
||||
|
||||
# get the claim id hash from txid bytes and int n
|
||||
def claim_id_hash(txid, n):
|
||||
return hash_160(txid + struct.pack('>I', n))
|
||||
|
||||
|
||||
# deocde a claim_id hex string
|
||||
def decode_claim_id_hex(claim_id_hex):
|
||||
claim_id = rev_hex(claim_id_hex).decode('hex')
|
||||
if len(claim_id) != CLAIM_ID_SIZE:
|
||||
raise InvalidClaimId()
|
||||
return claim_id
|
||||
|
||||
|
||||
# encode claim id bytes into hex string
|
||||
def encode_claim_id_hex(claim_id):
|
||||
return rev_hex(claim_id.encode('hex'))
|
||||
|
||||
|
||||
def strip_PKCS7_padding(s):
|
||||
"""return s stripped of PKCS7 padding"""
|
||||
if len(s) % 16 or not s:
|
||||
raise ValueError("String of len %d can't be PCKS7-padded" % len(s))
|
||||
numpads = ord(s[-1])
|
||||
if numpads > 16:
|
||||
raise ValueError("String ending with %r can't be PCKS7-padded" % s[-1])
|
||||
if s[-numpads:] != numpads * chr(numpads):
|
||||
raise ValueError("Invalid PKCS7 padding")
|
||||
return s[:-numpads]
|
||||
|
||||
|
||||
# backport padding fix to AES module
|
||||
aes.strip_PKCS7_padding = strip_PKCS7_padding
|
||||
|
||||
|
||||
def aes_encrypt_with_iv(key, iv, data):
|
||||
mode = aes.AESModeOfOperation.modeOfOperation["CBC"]
|
||||
key = map(ord, key)
|
||||
iv = map(ord, iv)
|
||||
data = aes.append_PKCS7_padding(data)
|
||||
keysize = len(key)
|
||||
assert keysize in aes.AES.keySize.values(), 'invalid key size: %s' % keysize
|
||||
moo = aes.AESModeOfOperation()
|
||||
(mode, length, ciph) = moo.encrypt(data, mode, key, keysize, iv)
|
||||
return ''.join(map(chr, ciph))
|
||||
|
||||
|
||||
def aes_decrypt_with_iv(key, iv, data):
|
||||
mode = aes.AESModeOfOperation.modeOfOperation["CBC"]
|
||||
key = map(ord, key)
|
||||
iv = map(ord, iv)
|
||||
keysize = len(key)
|
||||
assert keysize in aes.AES.keySize.values(), 'invalid key size: %s' % keysize
|
||||
data = map(ord, data)
|
||||
moo = aes.AESModeOfOperation()
|
||||
decr = moo.decrypt(data, None, mode, key, keysize, iv)
|
||||
decr = strip_PKCS7_padding(decr)
|
||||
return decr
|
||||
|
||||
|
||||
def pw_encode(s, password):
|
||||
if password:
|
||||
secret = Hash(password)
|
||||
return EncodeAES(secret, s.encode("utf8"))
|
||||
else:
|
||||
return s
|
||||
|
||||
|
||||
def pw_decode(s, password):
|
||||
if password is not None:
|
||||
secret = Hash(password)
|
||||
try:
|
||||
d = DecodeAES(secret, s).decode("utf8")
|
||||
except Exception:
|
||||
raise InvalidPassword()
|
||||
return d
|
||||
else:
|
||||
return s
|
||||
|
||||
|
||||
def op_push(i):
|
||||
if i < 0x4c:
|
||||
return int_to_hex(i)
|
||||
elif i < 0xff:
|
||||
return '4c' + int_to_hex(i)
|
||||
elif i < 0xffff:
|
||||
return '4d' + int_to_hex(i, 2)
|
||||
else:
|
||||
return '4e' + int_to_hex(i, 4)
|
||||
|
||||
|
||||
# pywallet openssl private key implementation
|
||||
|
||||
def i2o_ECPublicKey(pubkey, compressed=False):
|
||||
# public keys are 65 bytes long (520 bits)
|
||||
# 0x04 + 32-byte X-coordinate + 32-byte Y-coordinate
|
||||
# 0x00 = point at infinity, 0x02 and 0x03 = compressed, 0x04 = uncompressed
|
||||
# compressed keys: <sign> <x> where <sign> is 0x02 if y is even and 0x03 if y is odd
|
||||
if compressed:
|
||||
if pubkey.point.y() & 1:
|
||||
key = '03' + '%064x' % pubkey.point.x()
|
||||
else:
|
||||
key = '02' + '%064x' % pubkey.point.x()
|
||||
else:
|
||||
key = '04' + \
|
||||
'%064x' % pubkey.point.x() + \
|
||||
'%064x' % pubkey.point.y()
|
||||
|
||||
return key.decode('hex')
|
||||
|
||||
|
||||
# end pywallet openssl private key implementation
|
||||
# functions from pywallet
|
||||
|
||||
|
||||
def PrivKeyToSecret(privkey):
|
||||
return privkey[9:9 + 32]
|
||||
|
||||
|
||||
def SecretToASecret(secret, compressed=False, addrtype=0):
|
||||
vchIn = chr((addrtype + 128) & 255) + secret
|
||||
if compressed:
|
||||
vchIn += '\01'
|
||||
return b58encode_with_checksum(vchIn)
|
||||
|
||||
|
||||
def ASecretToSecret(key, addrtype=0):
|
||||
vch = b58decode_strip_checksum(key)
|
||||
if vch and vch[0] == chr((addrtype + 128) & 255):
|
||||
return vch[1:]
|
||||
elif is_minikey(key):
|
||||
return minikey_to_private_key(key)
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def regenerate_key(sec):
|
||||
b = ASecretToSecret(sec)
|
||||
if not b:
|
||||
return False
|
||||
b = b[0:32]
|
||||
return EC_KEY(b)
|
||||
|
||||
|
||||
def GetPubKey(pubkey, compressed=False):
|
||||
return i2o_ECPublicKey(pubkey, compressed)
|
||||
|
||||
|
||||
def GetSecret(pkey):
|
||||
return ('%064x' % pkey.secret).decode('hex')
|
||||
|
||||
|
||||
def is_compressed(sec):
|
||||
b = ASecretToSecret(sec)
|
||||
return len(b) == 33
|
||||
|
||||
|
||||
def public_key_from_private_key(sec):
|
||||
# rebuild public key from private key, compressed or uncompressed
|
||||
pkey = regenerate_key(sec)
|
||||
assert pkey
|
||||
compressed = is_compressed(sec)
|
||||
public_key = GetPubKey(pkey.pubkey, compressed)
|
||||
return public_key.encode('hex')
|
||||
|
||||
|
||||
def address_from_private_key(sec):
|
||||
public_key = public_key_from_private_key(sec)
|
||||
address = public_key_to_address(public_key.decode('hex'))
|
||||
return address
|
||||
|
||||
|
||||
def is_private_key(key):
|
||||
try:
|
||||
k = ASecretToSecret(key)
|
||||
return k is not False
|
||||
except:
|
||||
return False
|
||||
|
||||
# end pywallet functions
|
||||
|
||||
|
||||
def is_minikey(text):
|
||||
# Minikeys are typically 22 or 30 characters, but this routine
|
||||
# permits any length of 20 or more provided the minikey is valid.
|
||||
# A valid minikey must begin with an 'S', be in base58, and when
|
||||
# suffixed with '?' have its SHA256 hash begin with a zero byte.
|
||||
# They are widely used in Casascius physical bitoins.
|
||||
return (len(text) >= 20 and text[0] == 'S'
|
||||
and all(c in B58_CHARS for c in text)
|
||||
and ord(sha256(text + '?')[0]) == 0)
|
||||
|
||||
|
||||
def minikey_to_private_key(text):
|
||||
return sha256(text)
|
||||
|
||||
|
||||
def msg_magic(message):
|
||||
varint = var_int(len(message))
|
||||
encoded_varint = "".join([chr(int(varint[i:i + 2], 16)) for i in xrange(0, len(varint), 2)])
|
||||
return "\x18Bitcoin Signed Message:\n" + encoded_varint + message
|
||||
|
||||
|
||||
def verify_message(address, signature, message):
|
||||
try:
|
||||
EC_KEY.verify_message(address, signature, message)
|
||||
return True
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
|
||||
def encrypt_message(message, pubkey):
|
||||
return EC_KEY.encrypt_message(message, pubkey.decode('hex'))
|
||||
|
||||
|
||||
def chunks(l, n):
|
||||
return [l[i:i + n] for i in xrange(0, len(l), n)]
|
||||
|
||||
|
||||
def ECC_YfromX(x, curved=curve_secp256k1, odd=True):
|
||||
_p = curved.p()
|
||||
_a = curved.a()
|
||||
_b = curved.b()
|
||||
for offset in range(128):
|
||||
Mx = x + offset
|
||||
My2 = pow(Mx, 3, _p) + _a * pow(Mx, 2, _p) + _b % _p
|
||||
My = pow(My2, (_p + 1) / 4, _p)
|
||||
|
||||
if curved.contains_point(Mx, My):
|
||||
if odd == bool(My & 1):
|
||||
return [My, offset]
|
||||
return [_p - My, offset]
|
||||
raise Exception('ECC_YfromX: No Y found')
|
||||
|
||||
|
||||
def negative_point(P):
|
||||
return Point(P.curve(), P.x(), -P.y(), P.order())
|
||||
|
||||
|
||||
def point_to_ser(P, comp=True):
|
||||
if comp:
|
||||
return (('%02x' % (2 + (P.y() & 1))) + ('%064x' % P.x())).decode('hex')
|
||||
return ('04' + ('%064x' % P.x()) + ('%064x' % P.y())).decode('hex')
|
||||
|
||||
|
||||
def ser_to_point(Aser):
|
||||
curve = curve_secp256k1
|
||||
generator = generator_secp256k1
|
||||
_r = generator.order()
|
||||
assert Aser[0] in ['\x02', '\x03', '\x04']
|
||||
if Aser[0] == '\x04':
|
||||
return Point(curve, string_to_number(Aser[1:33]), string_to_number(Aser[33:]), _r)
|
||||
Mx = string_to_number(Aser[1:])
|
||||
return Point(curve, Mx, ECC_YfromX(Mx, curve, Aser[0] == '\x03')[0], _r)
|
||||
|
||||
|
||||
class MyVerifyingKey(ecdsa.VerifyingKey):
|
||||
@classmethod
|
||||
def from_signature(cls, sig, recid, h, curve):
|
||||
""" See http://www.secg.org/download/aid-780/sec1-v2.pdf, chapter 4.1.6 """
|
||||
curveFp = curve.curve
|
||||
G = curve.generator
|
||||
order = G.order()
|
||||
# extract r,s from signature
|
||||
r, s = util.sigdecode_string(sig, order)
|
||||
# 1.1
|
||||
x = r + (recid / 2) * order
|
||||
# 1.3
|
||||
alpha = (x * x * x + curveFp.a() * x + curveFp.b()) % curveFp.p()
|
||||
beta = msqr.modular_sqrt(alpha, curveFp.p())
|
||||
y = beta if (beta - recid) % 2 == 0 else curveFp.p() - beta
|
||||
# 1.4 the constructor checks that nR is at infinity
|
||||
R = Point(curveFp, x, y, order)
|
||||
# 1.5 compute e from message:
|
||||
e = string_to_number(h)
|
||||
minus_e = -e % order
|
||||
# 1.6 compute Q = r^-1 (sR - eG)
|
||||
inv_r = numbertheory.inverse_mod(r, order)
|
||||
Q = inv_r * (s * R + minus_e * G)
|
||||
return cls.from_public_point(Q, curve)
|
||||
|
||||
|
||||
class MySigningKey(ecdsa.SigningKey):
|
||||
"""Enforce low S values in signatures"""
|
||||
|
||||
def sign_number(self, number, entropy=None, k=None):
|
||||
curve = SECP256k1
|
||||
G = curve.generator
|
||||
order = G.order()
|
||||
r, s = ecdsa.SigningKey.sign_number(self, number, entropy, k)
|
||||
if s > order / 2:
|
||||
s = order - s
|
||||
return r, s
|
||||
|
||||
|
||||
class EC_KEY(object):
|
||||
def __init__(self, k):
|
||||
secret = string_to_number(k)
|
||||
self.pubkey = ecdsa.ecdsa.Public_key(generator_secp256k1, generator_secp256k1 * secret)
|
||||
self.privkey = ecdsa.ecdsa.Private_key(self.pubkey, secret)
|
||||
self.secret = secret
|
||||
|
||||
def get_public_key(self, compressed=True):
|
||||
return point_to_ser(self.pubkey.point, compressed).encode('hex')
|
||||
|
||||
def sign(self, msg_hash):
|
||||
private_key = MySigningKey.from_secret_exponent(self.secret, curve=SECP256k1)
|
||||
public_key = private_key.get_verifying_key()
|
||||
signature = private_key.sign_digest_deterministic(msg_hash, hashfunc=hashlib.sha256,
|
||||
sigencode=ecdsa.util.sigencode_string)
|
||||
assert public_key.verify_digest(signature, msg_hash, sigdecode=ecdsa.util.sigdecode_string)
|
||||
return signature
|
||||
|
||||
def sign_message(self, message, compressed, address):
|
||||
signature = self.sign(Hash(msg_magic(message)))
|
||||
for i in range(4):
|
||||
sig = chr(27 + i + (4 if compressed else 0)) + signature
|
||||
try:
|
||||
self.verify_message(address, sig, message)
|
||||
return sig
|
||||
except Exception:
|
||||
log.exception("error: cannot sign message")
|
||||
continue
|
||||
raise Exception("error: cannot sign message")
|
||||
|
||||
@classmethod
|
||||
def verify_message(cls, address, sig, message):
|
||||
if len(sig) != 65:
|
||||
raise Exception("Wrong encoding")
|
||||
nV = ord(sig[0])
|
||||
if nV < 27 or nV >= 35:
|
||||
raise Exception("Bad encoding")
|
||||
if nV >= 31:
|
||||
compressed = True
|
||||
nV -= 4
|
||||
else:
|
||||
compressed = False
|
||||
recid = nV - 27
|
||||
|
||||
h = Hash(msg_magic(message))
|
||||
public_key = MyVerifyingKey.from_signature(sig[1:], recid, h, curve=SECP256k1)
|
||||
# check public key
|
||||
public_key.verify_digest(sig[1:], h, sigdecode=ecdsa.util.sigdecode_string)
|
||||
pubkey = point_to_ser(public_key.pubkey.point, compressed)
|
||||
# check that we get the original signing address
|
||||
addr = public_key_to_address(pubkey)
|
||||
if address != addr:
|
||||
raise Exception("Bad signature")
|
||||
|
||||
# ECIES encryption/decryption methods; AES-128-CBC with PKCS7 is used as the cipher;
|
||||
# hmac-sha256 is used as the mac
|
||||
|
||||
@classmethod
|
||||
def encrypt_message(cls, message, pubkey):
|
||||
|
||||
pk = ser_to_point(pubkey)
|
||||
if not ecdsa.ecdsa.point_is_valid(generator_secp256k1, pk.x(), pk.y()):
|
||||
raise Exception('invalid pubkey')
|
||||
|
||||
ephemeral_exponent = number_to_string(ecdsa.util.randrange(pow(2, 256)),
|
||||
generator_secp256k1.order())
|
||||
ephemeral = EC_KEY(ephemeral_exponent)
|
||||
ecdh_key = point_to_ser(pk * ephemeral.privkey.secret_multiplier)
|
||||
key = hashlib.sha512(ecdh_key).digest()
|
||||
iv, key_e, key_m = key[0:16], key[16:32], key[32:]
|
||||
ciphertext = aes_encrypt_with_iv(key_e, iv, message)
|
||||
ephemeral_pubkey = ephemeral.get_public_key(compressed=True).decode('hex')
|
||||
encrypted = 'BIE1' + ephemeral_pubkey + ciphertext
|
||||
mac = hmac.new(key_m, encrypted, hashlib.sha256).digest()
|
||||
|
||||
return base64.b64encode(encrypted + mac)
|
||||
|
||||
def decrypt_message(self, encrypted):
|
||||
|
||||
encrypted = base64.b64decode(encrypted)
|
||||
|
||||
if len(encrypted) < 85:
|
||||
raise Exception('invalid ciphertext: length')
|
||||
|
||||
magic = encrypted[:4]
|
||||
ephemeral_pubkey = encrypted[4:37]
|
||||
ciphertext = encrypted[37:-32]
|
||||
mac = encrypted[-32:]
|
||||
|
||||
if magic != 'BIE1':
|
||||
raise Exception('invalid ciphertext: invalid magic bytes')
|
||||
|
||||
try:
|
||||
ephemeral_pubkey = ser_to_point(ephemeral_pubkey)
|
||||
except AssertionError, e:
|
||||
raise Exception('invalid ciphertext: invalid ephemeral pubkey')
|
||||
|
||||
if not ecdsa.ecdsa.point_is_valid(generator_secp256k1, ephemeral_pubkey.x(),
|
||||
ephemeral_pubkey.y()):
|
||||
raise Exception('invalid ciphertext: invalid ephemeral pubkey')
|
||||
|
||||
ecdh_key = point_to_ser(ephemeral_pubkey * self.privkey.secret_multiplier)
|
||||
key = hashlib.sha512(ecdh_key).digest()
|
||||
iv, key_e, key_m = key[0:16], key[16:32], key[32:]
|
||||
if mac != hmac.new(key_m, encrypted[:-32], hashlib.sha256).digest():
|
||||
raise Exception('invalid ciphertext: invalid mac')
|
||||
|
||||
return aes_decrypt_with_iv(key_e, iv, ciphertext)
|
||||
|
||||
|
||||
# BIP32
|
||||
|
||||
def random_seed(n):
|
||||
return "%032x" % ecdsa.util.randrange(pow(2, n))
|
||||
|
||||
|
||||
BIP32_PRIME = 0x80000000
|
||||
|
||||
|
||||
def get_pubkeys_from_secret(secret):
|
||||
# public key
|
||||
private_key = ecdsa.SigningKey.from_string(secret, curve=SECP256k1)
|
||||
public_key = private_key.get_verifying_key()
|
||||
K = public_key.to_string()
|
||||
K_compressed = GetPubKey(public_key.pubkey, True)
|
||||
return K, K_compressed
|
||||
|
||||
|
||||
# Child private key derivation function (from master private key)
|
||||
# k = master private key (32 bytes)
|
||||
# c = master chain code (extra entropy for key derivation) (32 bytes)
|
||||
# n = the index of the key we want to derive. (only 32 bits will be used)
|
||||
# If n is negative (i.e. the 32nd bit is set), the resulting private key's
|
||||
# corresponding public key can NOT be determined without the master private key.
|
||||
# However, if n is positive, the resulting private key's corresponding
|
||||
# public key can be determined without the master private key.
|
||||
def CKD_priv(k, c, n):
|
||||
is_prime = n & BIP32_PRIME
|
||||
return _CKD_priv(k, c, rev_hex(int_to_hex(n, 4)).decode('hex'), is_prime)
|
||||
|
||||
|
||||
def _CKD_priv(k, c, s, is_prime):
|
||||
order = generator_secp256k1.order()
|
||||
keypair = EC_KEY(k)
|
||||
cK = GetPubKey(keypair.pubkey, True)
|
||||
data = chr(0) + k + s if is_prime else cK + s
|
||||
I = hmac.new(c, data, hashlib.sha512).digest()
|
||||
k_n = number_to_string((string_to_number(I[0:32]) + string_to_number(k)) % order, order)
|
||||
c_n = I[32:]
|
||||
return k_n, c_n
|
||||
|
||||
|
||||
# Child public key derivation function (from public key only)
|
||||
# K = master public key
|
||||
# c = master chain code
|
||||
# n = index of key we want to derive
|
||||
# This function allows us to find the nth public key, as long as n is
|
||||
# non-negative. If n is negative, we need the master private key to find it.
|
||||
def CKD_pub(cK, c, n):
|
||||
if n & BIP32_PRIME:
|
||||
raise Exception("CKD pub error")
|
||||
return _CKD_pub(cK, c, rev_hex(int_to_hex(n, 4)).decode('hex'))
|
||||
|
||||
|
||||
# helper function, callable with arbitrary string
|
||||
def _CKD_pub(cK, c, s):
|
||||
order = generator_secp256k1.order()
|
||||
I = hmac.new(c, cK + s, hashlib.sha512).digest()
|
||||
curve = SECP256k1
|
||||
pubkey_point = string_to_number(I[0:32]) * curve.generator + ser_to_point(cK)
|
||||
public_key = ecdsa.VerifyingKey.from_public_point(pubkey_point, curve=SECP256k1)
|
||||
c_n = I[32:]
|
||||
cK_n = GetPubKey(public_key.pubkey, True)
|
||||
return cK_n, c_n
|
||||
|
||||
|
||||
BITCOIN_HEADER_PRIV = "0488ade4"
|
||||
BITCOIN_HEADER_PUB = "0488b21e"
|
||||
|
||||
TESTNET_HEADER_PRIV = "04358394"
|
||||
TESTNET_HEADER_PUB = "043587cf"
|
||||
|
||||
BITCOIN_HEADERS = (BITCOIN_HEADER_PUB, BITCOIN_HEADER_PRIV)
|
||||
TESTNET_HEADERS = (TESTNET_HEADER_PUB, TESTNET_HEADER_PRIV)
|
||||
|
||||
|
||||
def _get_headers(testnet):
|
||||
"""Returns the correct headers for either testnet or bitcoin, in the form
|
||||
of a 2-tuple, like (public, private)."""
|
||||
if testnet:
|
||||
return TESTNET_HEADERS
|
||||
else:
|
||||
return BITCOIN_HEADERS
|
||||
|
||||
|
||||
def deserialize_xkey(xkey):
|
||||
xkey = b58decode_strip_checksum(xkey)
|
||||
assert len(xkey) == 78
|
||||
|
||||
xkey_header = xkey[0:4].encode('hex')
|
||||
# Determine if the key is a bitcoin key or a testnet key.
|
||||
if xkey_header in TESTNET_HEADERS:
|
||||
head = TESTNET_HEADER_PRIV
|
||||
elif xkey_header in BITCOIN_HEADERS:
|
||||
head = BITCOIN_HEADER_PRIV
|
||||
else:
|
||||
raise Exception("Unknown xkey header: '%s'" % xkey_header)
|
||||
|
||||
depth = ord(xkey[4])
|
||||
fingerprint = xkey[5:9]
|
||||
child_number = xkey[9:13]
|
||||
c = xkey[13:13 + 32]
|
||||
if xkey[0:4].encode('hex') == head:
|
||||
K_or_k = xkey[13 + 33:]
|
||||
else:
|
||||
K_or_k = xkey[13 + 32:]
|
||||
return depth, fingerprint, child_number, c, K_or_k
|
||||
|
||||
|
||||
def get_xkey_name(xkey, testnet=False):
|
||||
depth, fingerprint, child_number, c, K = deserialize_xkey(xkey)
|
||||
n = int(child_number.encode('hex'), 16)
|
||||
if n & BIP32_PRIME:
|
||||
child_id = "%d'" % (n - BIP32_PRIME)
|
||||
else:
|
||||
child_id = "%d" % n
|
||||
if depth == 0:
|
||||
return ''
|
||||
elif depth == 1:
|
||||
return child_id
|
||||
else:
|
||||
raise BaseException("xpub depth error")
|
||||
|
||||
|
||||
def xpub_from_xprv(xprv, testnet=False):
|
||||
depth, fingerprint, child_number, c, k = deserialize_xkey(xprv)
|
||||
K, cK = get_pubkeys_from_secret(k)
|
||||
header_pub, _ = _get_headers(testnet)
|
||||
xpub = header_pub.decode('hex') + chr(depth) + fingerprint + child_number + c + cK
|
||||
return b58encode_with_checksum(xpub)
|
||||
|
||||
|
||||
def bip32_root(seed, testnet=False):
|
||||
header_pub, header_priv = _get_headers(testnet)
|
||||
I = hmac.new("Bitcoin seed", seed, hashlib.sha512).digest()
|
||||
master_k = I[0:32]
|
||||
master_c = I[32:]
|
||||
K, cK = get_pubkeys_from_secret(master_k)
|
||||
xprv = (header_priv + "00" + "00000000" + "00000000").decode("hex") + master_c + chr(
|
||||
0) + master_k
|
||||
xpub = (header_pub + "00" + "00000000" + "00000000").decode("hex") + master_c + cK
|
||||
return b58encode_with_checksum(xprv), b58encode_with_checksum(xpub)
|
||||
|
||||
|
||||
def xpub_from_pubkey(cK, testnet=False):
|
||||
header_pub, header_priv = _get_headers(testnet)
|
||||
assert cK[0] in ['\x02', '\x03']
|
||||
master_c = chr(0) * 32
|
||||
xpub = (header_pub + "00" + "00000000" + "00000000").decode("hex") + master_c + cK
|
||||
return b58encode_with_checksum(xpub)
|
||||
|
||||
|
||||
def bip32_private_derivation(xprv, branch, sequence, testnet=False):
|
||||
assert sequence.startswith(branch)
|
||||
if branch == sequence:
|
||||
return xprv, xpub_from_xprv(xprv, testnet)
|
||||
header_pub, header_priv = _get_headers(testnet)
|
||||
depth, fingerprint, child_number, c, k = deserialize_xkey(xprv)
|
||||
sequence = sequence[len(branch):]
|
||||
for n in sequence.split('/'):
|
||||
if n == '':
|
||||
continue
|
||||
i = int(n[:-1]) + BIP32_PRIME if n[-1] == "'" else int(n)
|
||||
parent_k = k
|
||||
k, c = CKD_priv(k, c, i)
|
||||
depth += 1
|
||||
|
||||
_, parent_cK = get_pubkeys_from_secret(parent_k)
|
||||
fingerprint = hash_160(parent_cK)[0:4]
|
||||
child_number = ("%08X" % i).decode('hex')
|
||||
K, cK = get_pubkeys_from_secret(k)
|
||||
xprv = header_priv.decode('hex') + chr(depth) + fingerprint + child_number + c + chr(0) + k
|
||||
xpub = header_pub.decode('hex') + chr(depth) + fingerprint + child_number + c + cK
|
||||
return b58encode_with_checksum(xprv), b58encode_with_checksum(xpub)
|
||||
|
||||
|
||||
def bip32_public_derivation(xpub, branch, sequence, testnet=False):
|
||||
header_pub, _ = _get_headers(testnet)
|
||||
depth, fingerprint, child_number, c, cK = deserialize_xkey(xpub)
|
||||
assert sequence.startswith(branch)
|
||||
sequence = sequence[len(branch):]
|
||||
for n in sequence.split('/'):
|
||||
if n == '':
|
||||
continue
|
||||
i = int(n)
|
||||
parent_cK = cK
|
||||
cK, c = CKD_pub(cK, c, i)
|
||||
depth += 1
|
||||
|
||||
fingerprint = hash_160(parent_cK)[0:4]
|
||||
child_number = ("%08X" % i).decode('hex')
|
||||
xpub = header_pub.decode('hex') + chr(depth) + fingerprint + child_number + c + cK
|
||||
return b58encode_with_checksum(xpub)
|
||||
|
||||
|
||||
def bip32_private_key(sequence, k, chain):
|
||||
for i in sequence:
|
||||
k, chain = CKD_priv(k, chain, i)
|
||||
return SecretToASecret(k, True)
|
|
@ -1,36 +1,78 @@
|
|||
import os
|
||||
import logging
|
||||
import hashlib
|
||||
from binascii import hexlify
|
||||
from operator import itemgetter
|
||||
|
||||
from twisted.internet import threads, defer
|
||||
|
||||
from lbryum.util import hex_to_int, int_to_hex, rev_hex
|
||||
from lbryum.hashing import hash_encode, Hash, PoWHash
|
||||
from .stream import StreamController, execute_serially
|
||||
from .constants import blockchain_params, HEADER_SIZE
|
||||
from lbrynet.wallet.stream import StreamController, execute_serially
|
||||
from lbrynet.wallet.transaction import Transaction
|
||||
from lbrynet.wallet.constants import CHAINS, MAIN_CHAIN, REGTEST_CHAIN, HEADER_SIZE
|
||||
from lbrynet.wallet.util import hex_to_int, int_to_hex, rev_hex, hash_encode
|
||||
from lbrynet.wallet.hash import double_sha256, pow_hash
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Transaction:
|
||||
class Address:
|
||||
|
||||
def __init__(self, tx_hash, raw, height):
|
||||
self.hash = tx_hash
|
||||
self.raw = raw
|
||||
self.height = height
|
||||
def __init__(self, address):
|
||||
self.address = address
|
||||
self.transactions = []
|
||||
|
||||
def add_transaction(self, transaction):
|
||||
self.transactions.append(transaction)
|
||||
|
||||
|
||||
class BlockchainTransactions:
|
||||
class Ledger:
|
||||
|
||||
def __init__(self, history):
|
||||
def __init__(self, config=None, db=None):
|
||||
self.config = config or {}
|
||||
self.db = db
|
||||
self.addresses = {}
|
||||
self.transactions = {}
|
||||
for address, transactions in history.items():
|
||||
self.addresses[address] = []
|
||||
for txid, raw, height in transactions:
|
||||
tx = Transaction(txid, raw, height)
|
||||
self.addresses[address].append(tx)
|
||||
self.transactions[txid] = tx
|
||||
self.headers = BlockchainHeaders(self.headers_path, self.config.get('chain', MAIN_CHAIN))
|
||||
self._on_transaction_controller = StreamController()
|
||||
self.on_transaction = self._on_transaction_controller.stream
|
||||
|
||||
@property
|
||||
def headers_path(self):
|
||||
filename = 'blockchain_headers'
|
||||
if self.config.get('chain', MAIN_CHAIN) != MAIN_CHAIN:
|
||||
filename = '{}_headers'.format(self.config['chain'])
|
||||
return os.path.join(self.config.get('wallet_path', ''), filename)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def load(self):
|
||||
txs = yield self.db.get_transactions()
|
||||
for tx_hash, raw, height in txs:
|
||||
self.transactions[tx_hash] = Transaction(raw, height)
|
||||
txios = yield self.db.get_transaction_inputs_and_outputs()
|
||||
for tx_hash, address_hash, input_output, amount, height in txios:
|
||||
tx = self.transactions[tx_hash]
|
||||
address = self.addresses.get(address_hash)
|
||||
if address is None:
|
||||
address = self.addresses[address_hash] = Address(address_hash)
|
||||
tx.add_txio(address, input_output, amount)
|
||||
address.add_transaction(tx)
|
||||
|
||||
def is_address_old(self, address, age_limit=2):
|
||||
age = -1
|
||||
for tx in self.get_transactions(address, []):
|
||||
if tx.height == 0:
|
||||
tx_age = 0
|
||||
else:
|
||||
tx_age = self.headers.height - tx.height + 1
|
||||
if tx_age > age:
|
||||
age = tx_age
|
||||
return age > age_limit
|
||||
|
||||
def add_transaction(self, address, transaction):
|
||||
self.transactions.setdefault(hexlify(transaction.id), transaction)
|
||||
self.addresses.setdefault(address, [])
|
||||
self.addresses[address].append(transaction)
|
||||
self._on_transaction_controller.add(transaction)
|
||||
|
||||
def has_address(self, address):
|
||||
return address in self.addresses
|
||||
|
@ -52,28 +94,39 @@ class BlockchainTransactions:
|
|||
def has_transaction(self, tx_hash):
|
||||
return tx_hash in self.transactions
|
||||
|
||||
def add_transaction(self, address, transaction):
|
||||
self.transactions.setdefault(transaction.hash, transaction)
|
||||
self.addresses.setdefault(address, [])
|
||||
self.addresses[address].append(transaction)
|
||||
def get_least_used_address(self, addresses, max_transactions=100):
|
||||
transaction_counts = []
|
||||
for address in addresses:
|
||||
transactions = self.get_transactions(address, [])
|
||||
tx_count = len(transactions)
|
||||
if tx_count == 0:
|
||||
return address
|
||||
elif tx_count >= max_transactions:
|
||||
continue
|
||||
else:
|
||||
transaction_counts.append((address, tx_count))
|
||||
if transaction_counts:
|
||||
transaction_counts.sort(key=itemgetter(1))
|
||||
return transaction_counts[0]
|
||||
|
||||
|
||||
class BlockchainHeaders:
|
||||
|
||||
def __init__(self, path, chain='lbrycrd_main'):
|
||||
def __init__(self, path, chain=MAIN_CHAIN):
|
||||
self.path = path
|
||||
self.chain = chain
|
||||
self.max_target = blockchain_params[chain]['max_target']
|
||||
self.target_timespan = blockchain_params[chain]['target_timespan']
|
||||
self.genesis_bits = blockchain_params[chain]['genesis_bits']
|
||||
self.max_target = CHAINS[chain]['max_target']
|
||||
self.target_timespan = CHAINS[chain]['target_timespan']
|
||||
self.genesis_bits = CHAINS[chain]['genesis_bits']
|
||||
|
||||
self._on_change_controller = StreamController()
|
||||
self.on_changed = self._on_change_controller.stream
|
||||
|
||||
self._size = None
|
||||
|
||||
if not os.path.exists(path):
|
||||
with open(path, 'wb'):
|
||||
def touch(self):
|
||||
if not os.path.exists(self.path):
|
||||
with open(self.path, 'wb'):
|
||||
pass
|
||||
|
||||
@property
|
||||
|
@ -175,12 +228,12 @@ class BlockchainHeaders:
|
|||
def _hash_header(self, header):
|
||||
if header is None:
|
||||
return '0' * 64
|
||||
return hash_encode(Hash(self._serialize(header).decode('hex')))
|
||||
return hash_encode(double_sha256(self._serialize(header).decode('hex')))
|
||||
|
||||
def _pow_hash_header(self, header):
|
||||
if header is None:
|
||||
return '0' * 64
|
||||
return hash_encode(PoWHash(self._serialize(header).decode('hex')))
|
||||
return hash_encode(pow_hash(self._serialize(header).decode('hex')))
|
||||
|
||||
def _calculate_lbry_next_work_required(self, height, first, last):
|
||||
""" See: lbrycrd/src/lbry.cpp """
|
||||
|
@ -189,7 +242,7 @@ class BlockchainHeaders:
|
|||
return self.genesis_bits, self.max_target
|
||||
|
||||
# bits to target
|
||||
if self.chain != 'lbrycrd_regtest':
|
||||
if self.chain != REGTEST_CHAIN:
|
||||
bits = last['bits']
|
||||
bitsN = (bits >> 24) & 0xff
|
||||
assert 0x03 <= bitsN <= 0x1f, \
|
|
@ -1,80 +1,72 @@
|
|||
import os
|
||||
import logging
|
||||
from binascii import unhexlify
|
||||
from operator import itemgetter
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
import lbryschema
|
||||
|
||||
from .protocol import Network
|
||||
from .blockchain import BlockchainHeaders, Transaction
|
||||
from .wallet import Wallet
|
||||
from .stream import execute_serially
|
||||
from lbrynet.wallet.wallet import Wallet
|
||||
from lbrynet.wallet.ledger import Ledger
|
||||
from lbrynet.wallet.protocol import Network
|
||||
from lbrynet.wallet.transaction import Transaction
|
||||
from lbrynet.wallet.stream import execute_serially
|
||||
from lbrynet.wallet.constants import MAXIMUM_FEE_PER_BYTE, MAXIMUM_FEE_PER_NAME_CHAR
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WalletManager:
|
||||
|
||||
def __init__(self, storage, config):
|
||||
self.storage = storage
|
||||
self.config = config
|
||||
lbryschema.BLOCKCHAIN_NAME = config['chain']
|
||||
self.headers = BlockchainHeaders(self.headers_path, config['chain'])
|
||||
self.wallet = Wallet(self.wallet_path, self.headers)
|
||||
self.network = Network(config)
|
||||
def __init__(self, config=None, wallet=None, ledger=None, network=None):
|
||||
self.config = config or {}
|
||||
self.ledger = ledger or Ledger(self.config)
|
||||
self.wallet = wallet or Wallet()
|
||||
self.wallets = [self.wallet]
|
||||
self.network = network or Network(self.config)
|
||||
self.network.on_header.listen(self.process_header)
|
||||
self.network.on_status.listen(self.process_status)
|
||||
|
||||
@property
|
||||
def headers_path(self):
|
||||
filename = 'blockchain_headers'
|
||||
if self.config['chain'] != 'lbrycrd_main':
|
||||
filename = '{}_headers'.format(self.config['chain'].split("_")[1])
|
||||
return os.path.join(self.config['wallet_path'], filename)
|
||||
def fee_per_byte(self):
|
||||
return self.config.get('fee_per_byte', MAXIMUM_FEE_PER_BYTE)
|
||||
|
||||
@property
|
||||
def wallet_path(self):
|
||||
return os.path.join(self.config['wallet_path'], 'wallets', 'default_wallet')
|
||||
def fee_per_name_char(self):
|
||||
return self.config.get('fee_per_name_char', MAXIMUM_FEE_PER_NAME_CHAR)
|
||||
|
||||
@property
|
||||
def addresses_without_history(self):
|
||||
for wallet in self.wallets:
|
||||
for address in wallet.addresses:
|
||||
if not self.ledger.has_address(address):
|
||||
yield address
|
||||
|
||||
def get_least_used_receiving_address(self, max_transactions=1000):
|
||||
return self._get_least_used_address(
|
||||
self.wallet.receiving_addresses,
|
||||
self.wallet.default_account.receiving,
|
||||
self.wallet.default_account.receiving_keys.addresses,
|
||||
self.wallet.default_account.receiving_keys,
|
||||
max_transactions
|
||||
)
|
||||
|
||||
def get_least_used_change_address(self, max_transactions=100):
|
||||
return self._get_least_used_address(
|
||||
self.wallet.change_addresses,
|
||||
self.wallet.default_account.change,
|
||||
self.wallet.default_account.change_keys.addresses,
|
||||
self.wallet.default_account.change_keys,
|
||||
max_transactions
|
||||
)
|
||||
|
||||
def _get_least_used_address(self, addresses, sequence, max_transactions):
|
||||
transaction_counts = []
|
||||
for address in addresses:
|
||||
transactions = self.wallet.history.get_transactions(address, [])
|
||||
tx_count = len(transactions)
|
||||
if tx_count == 0:
|
||||
return address
|
||||
elif tx_count >= max_transactions:
|
||||
continue
|
||||
else:
|
||||
transaction_counts.append((address, tx_count))
|
||||
|
||||
if transaction_counts:
|
||||
transaction_counts.sort(key=itemgetter(1))
|
||||
return transaction_counts[0]
|
||||
|
||||
address = self.ledger.get_least_used_address(addresses, max_transactions)
|
||||
if address:
|
||||
return address
|
||||
address = sequence.generate_next_address()
|
||||
self.subscribe_history(address)
|
||||
return address
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def start(self):
|
||||
first_connection = self.network.on_connected.first
|
||||
self.network.start()
|
||||
yield self.network.on_connected.first
|
||||
yield first_connection
|
||||
self.ledger.headers.touch()
|
||||
yield self.update_headers()
|
||||
yield self.network.subscribe_headers()
|
||||
yield self.update_wallet()
|
||||
|
@ -86,38 +78,34 @@ class WalletManager:
|
|||
@defer.inlineCallbacks
|
||||
def update_headers(self):
|
||||
while True:
|
||||
height_sought = len(self.headers)
|
||||
height_sought = len(self.ledger.headers)
|
||||
headers = yield self.network.get_headers(height_sought)
|
||||
log.info("received {} headers starting at {} height".format(headers['count'], height_sought))
|
||||
if headers['count'] <= 0:
|
||||
break
|
||||
yield self.headers.connect(height_sought, headers['hex'].decode('hex'))
|
||||
yield self.ledger.headers.connect(height_sought, headers['hex'].decode('hex'))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def process_header(self, response):
|
||||
header = response[0]
|
||||
if self.update_headers.is_running:
|
||||
return
|
||||
if header['height'] == len(self.headers):
|
||||
if header['height'] == len(self.ledger.headers):
|
||||
# New header from network directly connects after the last local header.
|
||||
yield self.headers.connect(len(self.headers), header['hex'].decode('hex'))
|
||||
elif header['height'] > len(self.headers):
|
||||
yield self.ledger.headers.connect(len(self.ledger.headers), header['hex'].decode('hex'))
|
||||
elif header['height'] > len(self.ledger.headers):
|
||||
# New header is several heights ahead of local, do download instead.
|
||||
yield self.update_headers()
|
||||
|
||||
@execute_serially
|
||||
@defer.inlineCallbacks
|
||||
def update_wallet(self):
|
||||
|
||||
if not self.wallet.exists:
|
||||
self.wallet.create()
|
||||
|
||||
# Before subscribing, download history for any addresses that don't have any,
|
||||
# this avoids situation where we're getting status updates to addresses we know
|
||||
# need to update anyways. Continue to get history and create more addresses until
|
||||
# all missing addresses are created and history for them is fully restored.
|
||||
self.wallet.ensure_enough_addresses()
|
||||
addresses = list(self.wallet.addresses_without_history)
|
||||
addresses = list(self.addresses_without_history)
|
||||
while addresses:
|
||||
yield defer.gatherResults([
|
||||
self.update_history(a) for a in addresses
|
||||
|
@ -135,19 +123,19 @@ class WalletManager:
|
|||
def update_history(self, address):
|
||||
history = yield self.network.get_history(address)
|
||||
for hash in map(itemgetter('tx_hash'), history):
|
||||
transaction = self.wallet.history.get_transaction(hash)
|
||||
transaction = self.ledger.get_transaction(hash)
|
||||
if not transaction:
|
||||
raw = yield self.network.get_transaction(hash)
|
||||
transaction = Transaction(hash, raw, None)
|
||||
self.wallet.history.add_transaction(address, transaction)
|
||||
transaction = Transaction(unhexlify(raw))
|
||||
self.ledger.add_transaction(address, transaction)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def subscribe_history(self, address):
|
||||
status = yield self.network.subscribe_address(address)
|
||||
if status != self.wallet.history.get_status(address):
|
||||
if status != self.ledger.get_status(address):
|
||||
self.update_history(address)
|
||||
|
||||
def process_status(self, response):
|
||||
address, status = response
|
||||
if status != self.wallet.history.get_status(address):
|
||||
if status != self.ledger.get_status(address):
|
||||
self.update_history(address)
|
||||
|
|
|
@ -5,14 +5,12 @@ import os
|
|||
import pkgutil
|
||||
import string
|
||||
import unicodedata
|
||||
import logging
|
||||
import ecdsa
|
||||
import pbkdf2
|
||||
|
||||
from . import constants
|
||||
from .hashing import hmac_sha_512
|
||||
from .hash import hmac_sha512
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# http://www.asahi-net.or.jp/~ax2s-kmtn/ref/unicode/e_asia.html
|
||||
CJK_INTERVALS = [
|
||||
|
@ -98,10 +96,9 @@ class Mnemonic:
|
|||
assert ' ' not in line
|
||||
if line:
|
||||
self.wordlist.append(line)
|
||||
log.info("wordlist has %d words", len(self.wordlist))
|
||||
|
||||
@classmethod
|
||||
def mnemonic_to_seed(cls, mnemonic, passphrase):
|
||||
def mnemonic_to_seed(cls, mnemonic, passphrase=''):
|
||||
PBKDF2_ROUNDS = 2048
|
||||
mnemonic = prepare_seed(mnemonic)
|
||||
return pbkdf2.PBKDF2(mnemonic, 'lbryum' + passphrase, iterations=PBKDF2_ROUNDS,
|
||||
|
@ -137,7 +134,6 @@ class Mnemonic:
|
|||
k = len(prefix) * 4
|
||||
# we add at least 16 bits
|
||||
n_added = max(16, k + num_bits - n)
|
||||
log.info("make_seed %s adding %d bits", prefix, n_added)
|
||||
my_entropy = ecdsa.util.randrange(pow(2, n_added))
|
||||
nonce = 0
|
||||
while True:
|
||||
|
@ -147,11 +143,10 @@ class Mnemonic:
|
|||
assert i == self.mnemonic_decode(seed)
|
||||
if is_new_seed(seed, prefix):
|
||||
break
|
||||
log.info('%d words', len(seed.split()))
|
||||
return seed
|
||||
|
||||
|
||||
def is_new_seed(x, prefix=constants.SEED_PREFIX):
|
||||
x = prepare_seed(x)
|
||||
s = hmac_sha_512("Seed version", x.encode('utf8')).encode('hex')
|
||||
s = hmac_sha512("Seed version", x.encode('utf8')).encode('hex')
|
||||
return s.startswith(prefix)
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
import struct
|
||||
from .enumeration import Enumeration
|
||||
|
||||
opcodes = Enumeration("Opcodes", [
|
||||
("OP_0", 0), ("OP_PUSHDATA1", 76), "OP_PUSHDATA2", "OP_PUSHDATA4", "OP_1NEGATE", "OP_RESERVED",
|
||||
"OP_1", "OP_2", "OP_3", "OP_4", "OP_5", "OP_6", "OP_7",
|
||||
"OP_8", "OP_9", "OP_10", "OP_11", "OP_12", "OP_13", "OP_14", "OP_15", "OP_16",
|
||||
"OP_NOP", "OP_VER", "OP_IF", "OP_NOTIF", "OP_VERIF", "OP_VERNOTIF", "OP_ELSE", "OP_ENDIF",
|
||||
"OP_VERIFY",
|
||||
"OP_RETURN", "OP_TOALTSTACK", "OP_FROMALTSTACK", "OP_2DROP", "OP_2DUP", "OP_3DUP", "OP_2OVER",
|
||||
"OP_2ROT", "OP_2SWAP",
|
||||
"OP_IFDUP", "OP_DEPTH", "OP_DROP", "OP_DUP", "OP_NIP", "OP_OVER", "OP_PICK", "OP_ROLL",
|
||||
"OP_ROT",
|
||||
"OP_SWAP", "OP_TUCK", "OP_CAT", "OP_SUBSTR", "OP_LEFT", "OP_RIGHT", "OP_SIZE", "OP_INVERT",
|
||||
"OP_AND",
|
||||
"OP_OR", "OP_XOR", "OP_EQUAL", "OP_EQUALVERIFY", "OP_RESERVED1", "OP_RESERVED2", "OP_1ADD",
|
||||
"OP_1SUB", "OP_2MUL",
|
||||
"OP_2DIV", "OP_NEGATE", "OP_ABS", "OP_NOT", "OP_0NOTEQUAL", "OP_ADD", "OP_SUB", "OP_MUL",
|
||||
"OP_DIV",
|
||||
"OP_MOD", "OP_LSHIFT", "OP_RSHIFT", "OP_BOOLAND", "OP_BOOLOR",
|
||||
"OP_NUMEQUAL", "OP_NUMEQUALVERIFY", "OP_NUMNOTEQUAL", "OP_LESSTHAN",
|
||||
"OP_GREATERTHAN", "OP_LESSTHANOREQUAL", "OP_GREATERTHANOREQUAL", "OP_MIN", "OP_MAX",
|
||||
"OP_WITHIN", "OP_RIPEMD160", "OP_SHA1", "OP_SHA256", "OP_HASH160",
|
||||
"OP_HASH256", "OP_CODESEPARATOR", "OP_CHECKSIG", "OP_CHECKSIGVERIFY", "OP_CHECKMULTISIG",
|
||||
"OP_CHECKMULTISIGVERIFY", "OP_NOP1", "OP_NOP2", "OP_NOP3", "OP_NOP4", "OP_NOP5",
|
||||
"OP_CLAIM_NAME",
|
||||
"OP_SUPPORT_CLAIM", "OP_UPDATE_CLAIM",
|
||||
("OP_SINGLEBYTE_END", 0xF0),
|
||||
("OP_DOUBLEBYTE_BEGIN", 0xF000),
|
||||
"OP_PUBKEY", "OP_PUBKEYHASH",
|
||||
("OP_INVALIDOPCODE", 0xFFFF),
|
||||
])
|
||||
|
||||
|
||||
def script_GetOp(bytes):
|
||||
i = 0
|
||||
while i < len(bytes):
|
||||
vch = None
|
||||
opcode = ord(bytes[i])
|
||||
i += 1
|
||||
if opcode >= opcodes.OP_SINGLEBYTE_END:
|
||||
opcode <<= 8
|
||||
opcode |= ord(bytes[i])
|
||||
i += 1
|
||||
|
||||
if opcode <= opcodes.OP_PUSHDATA4:
|
||||
nSize = opcode
|
||||
if opcode == opcodes.OP_PUSHDATA1:
|
||||
nSize = ord(bytes[i])
|
||||
i += 1
|
||||
elif opcode == opcodes.OP_PUSHDATA2:
|
||||
(nSize,) = struct.unpack_from('<H', bytes, i)
|
||||
i += 2
|
||||
elif opcode == opcodes.OP_PUSHDATA4:
|
||||
(nSize,) = struct.unpack_from('<I', bytes, i)
|
||||
i += 4
|
||||
vch = bytes[i:i + nSize]
|
||||
i += nSize
|
||||
|
||||
yield (opcode, vch, i)
|
||||
|
||||
|
||||
def script_GetOpName(opcode):
|
||||
return (opcodes.whatis(opcode)).replace("OP_", "")
|
||||
|
||||
|
||||
def match_decoded(decoded, to_match):
|
||||
if len(decoded) != len(to_match):
|
||||
return False
|
||||
for i, d in enumerate(decoded):
|
||||
if to_match[i] == opcodes.OP_PUSHDATA4 and opcodes.OP_PUSHDATA4 >= d[0] > 0:
|
||||
# Opcodes below OP_PUSHDATA4 all just push data onto stack, # and are equivalent.
|
||||
continue
|
||||
if to_match[i] != decoded[i][0]:
|
||||
return False
|
||||
return True
|
|
@ -1,10 +1,9 @@
|
|||
import sys
|
||||
import time
|
||||
import six
|
||||
import json
|
||||
import socket
|
||||
import logging
|
||||
from itertools import cycle
|
||||
from twisted.internet import defer, reactor, protocol, threads
|
||||
from twisted.internet import defer, reactor, protocol
|
||||
from twisted.application.internet import ClientService, CancelledError
|
||||
from twisted.internet.endpoints import clientFromString
|
||||
from twisted.protocols.basic import LineOnlyReceiver
|
||||
|
@ -16,6 +15,12 @@ from .stream import StreamController
|
|||
log = logging.getLogger()
|
||||
|
||||
|
||||
def unicode2bytes(string):
|
||||
if isinstance(string, six.text_type):
|
||||
return string.encode('iso-8859-1')
|
||||
return string
|
||||
|
||||
|
||||
class StratumClientProtocol(LineOnlyReceiver):
|
||||
delimiter = '\n'
|
||||
|
||||
|
@ -65,7 +70,14 @@ class StratumClientProtocol(LineOnlyReceiver):
|
|||
def lineReceived(self, line):
|
||||
|
||||
try:
|
||||
message = json.loads(line)
|
||||
# `line` comes in as a byte string but `json.loads` automatically converts everything to
|
||||
# unicode. For keys it's not a big deal but for values there is an expectation
|
||||
# everywhere else in wallet code that most values are byte strings.
|
||||
message = json.loads(
|
||||
line, object_hook=lambda obj: {
|
||||
k: unicode2bytes(v) for k, v in obj.items()
|
||||
}
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
raise ProtocolException("Cannot decode message '{}'".format(line.strip()))
|
||||
|
||||
|
@ -137,7 +149,7 @@ class Network:
|
|||
|
||||
@defer.inlineCallbacks
|
||||
def start(self):
|
||||
for server in cycle(self.config.get('default_servers')):
|
||||
for server in cycle(self.config['default_servers']):
|
||||
endpoint = clientFromString(reactor, 'tcp:{}:{}'.format(*server))
|
||||
self.service = ClientService(endpoint, StratumClientFactory(self))
|
||||
self.service.startService()
|
||||
|
|
|
@ -90,10 +90,30 @@ def read_small_integer(token):
|
|||
return (token - OP_1) + 1
|
||||
|
||||
|
||||
# tokens contain parsed values to be matched against opcodes
|
||||
Token = namedtuple('Token', 'value')
|
||||
DataToken = subclass_tuple('DataToken', Token)
|
||||
SmallIntegerToken = subclass_tuple('SmallIntegerToken', Token)
|
||||
class Token(namedtuple('Token', 'value')):
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self):
|
||||
name = None
|
||||
for var_name, var_value in globals().items():
|
||||
if var_name.startswith('OP_') and var_value == self.value:
|
||||
name = var_name
|
||||
break
|
||||
return name or self.value
|
||||
|
||||
|
||||
class DataToken(Token):
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self):
|
||||
return '"{}"'.format(hexlify(self.value))
|
||||
|
||||
|
||||
class SmallIntegerToken(Token):
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self):
|
||||
return 'SmallIntegerToken({})'.format(self.value)
|
||||
|
||||
|
||||
def token_producer(source):
|
||||
|
@ -259,11 +279,13 @@ class Script(object):
|
|||
self.template = template
|
||||
self.values = values
|
||||
if source:
|
||||
self._parse(template_hint)
|
||||
self.parse(template_hint)
|
||||
elif template and values:
|
||||
self.source = template.generate(values)
|
||||
else:
|
||||
raise ValueError("Either a valid 'source' or a 'template' and 'values' are required.")
|
||||
self.generate()
|
||||
|
||||
@property
|
||||
def tokens(self):
|
||||
return tokenize(BCDataStream(self.source))
|
||||
|
||||
@classmethod
|
||||
def from_source_with_template(cls, source, template):
|
||||
|
@ -274,8 +296,8 @@ class Script(object):
|
|||
else:
|
||||
return cls(source, template_hint=template)
|
||||
|
||||
def _parse(self, template_hint=None):
|
||||
tokens = tokenize(BCDataStream(self.source))
|
||||
def parse(self, template_hint=None):
|
||||
tokens = self.tokens
|
||||
for template in chain((template_hint,), self.templates):
|
||||
if not template:
|
||||
continue
|
||||
|
@ -287,12 +309,18 @@ class Script(object):
|
|||
continue
|
||||
raise ValueError('No matching templates for source: {}'.format(hexlify(self.source)))
|
||||
|
||||
def generate(self):
|
||||
self.source = self.template.generate(self.values)
|
||||
|
||||
|
||||
class InputScript(Script):
|
||||
""" Input / redeem script templates (aka scriptSig) """
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
# input / redeem script templates (aka scriptSig)
|
||||
REDEEM_PUBKEY = Template('pubkey', (
|
||||
PUSH_SINGLE('signature'),
|
||||
))
|
||||
REDEEM_PUBKEY_HASH = Template('pubkey_hash', (
|
||||
PUSH_SINGLE('signature'), PUSH_SINGLE('pubkey')
|
||||
))
|
||||
|
@ -305,6 +333,7 @@ class InputScript(Script):
|
|||
))
|
||||
|
||||
templates = [
|
||||
REDEEM_PUBKEY,
|
||||
REDEEM_PUBKEY_HASH,
|
||||
REDEEM_SCRIPT_HASH,
|
||||
REDEEM_SCRIPT
|
||||
|
@ -409,6 +438,14 @@ class OutputScript(Script):
|
|||
'pubkey_hash': pubkey_hash
|
||||
})
|
||||
|
||||
@property
|
||||
def is_pay_pubkey_hash(self):
|
||||
return self.template.name.endswith('pay_pubkey_hash')
|
||||
|
||||
@property
|
||||
def is_pay_script_hash(self):
|
||||
return self.template.name.endswith('pay_script_hash')
|
||||
|
||||
@property
|
||||
def is_claim_name(self):
|
||||
return self.template.name.startswith('claim_name+')
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import os
|
||||
import json
|
||||
|
||||
|
||||
class JSONStore(dict):
|
||||
|
||||
def __init__(self, config, name):
|
||||
self.config = config
|
||||
self.path = os.path.join(self.config.path, name)
|
||||
self.load()
|
||||
|
||||
def load(self):
|
||||
try:
|
||||
with open(self.path, 'r') as f:
|
||||
self.update(json.loads(f.read()))
|
||||
except:
|
||||
pass
|
||||
|
||||
def save(self):
|
||||
with open(self.path, 'w') as f:
|
||||
s = json.dumps(self, indent=4, sort_keys=True)
|
||||
r = f.write(s)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
dict.__setitem__(self, key, value)
|
||||
self.save()
|
||||
|
||||
def pop(self, key):
|
||||
if key in self.keys():
|
||||
dict.pop(self, key)
|
||||
self.save()
|
|
@ -1,701 +1,346 @@
|
|||
import sys
|
||||
import hashlib
|
||||
import io
|
||||
import six
|
||||
import logging
|
||||
import ecdsa
|
||||
from ecdsa.curves import SECP256k1
|
||||
from binascii import hexlify
|
||||
from typing import List
|
||||
|
||||
from lbryschema.address import hash_160_bytes_to_address, public_key_to_address
|
||||
from lbryschema.address import address_to_hash_160
|
||||
from lbrynet.wallet import get_wallet_manager
|
||||
from lbrynet.wallet.bcd_data_stream import BCDataStream
|
||||
from lbrynet.wallet.hash import sha256, hash160_to_address, claim_id_hash
|
||||
from lbrynet.wallet.script import InputScript, OutputScript
|
||||
from lbrynet.wallet.wallet import Wallet
|
||||
|
||||
from .constants import TYPE_SCRIPT, TYPE_PUBKEY, TYPE_UPDATE, TYPE_SUPPORT, TYPE_CLAIM
|
||||
from .constants import TYPE_ADDRESS, NO_SIGNATURE
|
||||
from .opcodes import opcodes, match_decoded, script_GetOp
|
||||
from .bcd_data_stream import BCDataStream
|
||||
from .hashing import Hash, hash_160, hash_encode
|
||||
from .lbrycrd import op_push
|
||||
from .lbrycrd import point_to_ser, MyVerifyingKey, MySigningKey
|
||||
from .lbrycrd import regenerate_key, public_key_from_private_key
|
||||
from .lbrycrd import encode_claim_id_hex, claim_id_hash
|
||||
from .util import var_int, int_to_hex, parse_sig, rev_hex
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
|
||||
def parse_xpub(x_pubkey):
|
||||
if x_pubkey[0:2] in ['02', '03', '04']:
|
||||
pubkey = x_pubkey
|
||||
elif x_pubkey[0:2] == 'ff':
|
||||
from lbryum.bip32 import BIP32_Account
|
||||
xpub, s = BIP32_Account.parse_xpubkey(x_pubkey)
|
||||
pubkey = BIP32_Account.derive_pubkey_from_xpub(xpub, s[0], s[1])
|
||||
elif x_pubkey[0:2] == 'fd':
|
||||
addrtype = ord(x_pubkey[2:4].decode('hex'))
|
||||
hash160 = x_pubkey[4:].decode('hex')
|
||||
pubkey = None
|
||||
address = hash_160_bytes_to_address(hash160, addrtype)
|
||||
else:
|
||||
raise BaseException("Cannnot parse pubkey")
|
||||
if pubkey:
|
||||
address = public_key_to_address(pubkey.decode('hex'))
|
||||
return pubkey, address
|
||||
NULL_HASH = '\x00'*32
|
||||
|
||||
|
||||
def parse_scriptSig(d, bytes):
|
||||
try:
|
||||
decoded = [x for x in script_GetOp(bytes)]
|
||||
except Exception:
|
||||
# coinbase transactions raise an exception
|
||||
log.error("cannot find address in input script: {}".format(bytes.encode('hex')))
|
||||
return
|
||||
class InputOutput(object):
|
||||
|
||||
# payto_pubkey
|
||||
match = [opcodes.OP_PUSHDATA4]
|
||||
if match_decoded(decoded, match):
|
||||
sig = decoded[0][1].encode('hex')
|
||||
d['address'] = "(pubkey)"
|
||||
d['signatures'] = [sig]
|
||||
d['num_sig'] = 1
|
||||
d['x_pubkeys'] = ["(pubkey)"]
|
||||
d['pubkeys'] = ["(pubkey)"]
|
||||
return
|
||||
@property
|
||||
def fee(self):
|
||||
""" Fee based on size of the input / output. """
|
||||
return get_wallet_manager().fee_per_byte * self.size
|
||||
|
||||
# non-generated TxIn transactions push a signature
|
||||
# (seventy-something bytes) and then their public key
|
||||
# (65 bytes) onto the stack:
|
||||
match = [opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4]
|
||||
if match_decoded(decoded, match):
|
||||
sig = decoded[0][1].encode('hex')
|
||||
x_pubkey = decoded[1][1].encode('hex')
|
||||
try:
|
||||
signatures = parse_sig([sig])
|
||||
pubkey, address = parse_xpub(x_pubkey)
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
log.error("cannot find address in input script: {}".format(bytes.encode('hex')))
|
||||
return
|
||||
d['signatures'] = signatures
|
||||
d['x_pubkeys'] = [x_pubkey]
|
||||
d['num_sig'] = 1
|
||||
d['pubkeys'] = [pubkey]
|
||||
d['address'] = address
|
||||
return
|
||||
@property
|
||||
def size(self):
|
||||
""" Size of this input / output in bytes. """
|
||||
stream = BCDataStream()
|
||||
self.serialize_to(stream)
|
||||
return len(stream.get_bytes())
|
||||
|
||||
# p2sh transaction, m of n
|
||||
match = [opcodes.OP_0] + [opcodes.OP_PUSHDATA4] * (len(decoded) - 1)
|
||||
if not match_decoded(decoded, match):
|
||||
log.error("cannot find address in input script: {}".format(bytes.encode('hex')))
|
||||
return
|
||||
x_sig = [x[1].encode('hex') for x in decoded[1:-1]]
|
||||
dec2 = [x for x in script_GetOp(decoded[-1][1])]
|
||||
m = dec2[0][0] - opcodes.OP_1 + 1
|
||||
n = dec2[-2][0] - opcodes.OP_1 + 1
|
||||
op_m = opcodes.OP_1 + m - 1
|
||||
op_n = opcodes.OP_1 + n - 1
|
||||
match_multisig = [op_m] + [opcodes.OP_PUSHDATA4] * n + [op_n, opcodes.OP_CHECKMULTISIG]
|
||||
if not match_decoded(dec2, match_multisig):
|
||||
log.error("cannot find address in input script: {}".format(bytes.encode('hex')))
|
||||
return
|
||||
x_pubkeys = map(lambda x: x[1].encode('hex'), dec2[1:-2])
|
||||
pubkeys = [parse_xpub(x)[0] for x in x_pubkeys] # xpub, addr = parse_xpub()
|
||||
redeemScript = Transaction.multisig_script(pubkeys, m)
|
||||
# write result in d
|
||||
d['num_sig'] = m
|
||||
d['signatures'] = parse_sig(x_sig)
|
||||
d['x_pubkeys'] = x_pubkeys
|
||||
d['pubkeys'] = pubkeys
|
||||
d['redeemScript'] = redeemScript
|
||||
d['address'] = hash_160_bytes_to_address(hash_160(redeemScript.decode('hex')), 5)
|
||||
def serialize_to(self, stream):
|
||||
raise NotImplemented
|
||||
|
||||
|
||||
class NameClaim(object):
|
||||
def __init__(self, name, value):
|
||||
self.name = name
|
||||
self.value = value
|
||||
class Input(InputOutput):
|
||||
|
||||
NULL_SIGNATURE = '0'*72
|
||||
NULL_PUBLIC_KEY = '0'*33
|
||||
|
||||
class ClaimUpdate(object):
|
||||
def __init__(self, name, claim_id, value):
|
||||
self.name = name
|
||||
self.claim_id = claim_id
|
||||
self.value = value
|
||||
|
||||
|
||||
class ClaimSupport(object):
|
||||
def __init__(self, name, claim_id):
|
||||
self.name = name
|
||||
self.claim_id = claim_id
|
||||
|
||||
|
||||
def decode_claim_script(decoded_script):
|
||||
if len(decoded_script) <= 6:
|
||||
return False
|
||||
op = 0
|
||||
claim_type = decoded_script[op][0]
|
||||
if claim_type == opcodes.OP_UPDATE_CLAIM:
|
||||
if len(decoded_script) <= 7:
|
||||
return False
|
||||
if claim_type not in [
|
||||
opcodes.OP_CLAIM_NAME,
|
||||
opcodes.OP_SUPPORT_CLAIM,
|
||||
opcodes.OP_UPDATE_CLAIM
|
||||
]:
|
||||
return False
|
||||
op += 1
|
||||
value = None
|
||||
claim_id = None
|
||||
claim = None
|
||||
if not 0 <= decoded_script[op][0] <= opcodes.OP_PUSHDATA4:
|
||||
return False
|
||||
name = decoded_script[op][1]
|
||||
op += 1
|
||||
if not 0 <= decoded_script[op][0] <= opcodes.OP_PUSHDATA4:
|
||||
return False
|
||||
if decoded_script[0][0] in [
|
||||
opcodes.OP_SUPPORT_CLAIM,
|
||||
opcodes.OP_UPDATE_CLAIM
|
||||
]:
|
||||
claim_id = decoded_script[op][1]
|
||||
if len(claim_id) != 20:
|
||||
return False
|
||||
else:
|
||||
value = decoded_script[op][1]
|
||||
op += 1
|
||||
if decoded_script[0][0] == opcodes.OP_UPDATE_CLAIM:
|
||||
value = decoded_script[op][1]
|
||||
op += 1
|
||||
if decoded_script[op][0] != opcodes.OP_2DROP:
|
||||
return False
|
||||
op += 1
|
||||
if decoded_script[op][0] != opcodes.OP_DROP and decoded_script[0][0] == opcodes.OP_CLAIM_NAME:
|
||||
return False
|
||||
elif decoded_script[op][0] != opcodes.OP_2DROP and decoded_script[0][0] == \
|
||||
opcodes.OP_UPDATE_CLAIM:
|
||||
return False
|
||||
op += 1
|
||||
if decoded_script[0][0] == opcodes.OP_CLAIM_NAME:
|
||||
if name is None or value is None:
|
||||
return False
|
||||
claim = NameClaim(name, value)
|
||||
elif decoded_script[0][0] == opcodes.OP_UPDATE_CLAIM:
|
||||
if name is None or value is None or claim_id is None:
|
||||
return False
|
||||
claim = ClaimUpdate(name, claim_id, value)
|
||||
elif decoded_script[0][0] == opcodes.OP_SUPPORT_CLAIM:
|
||||
if name is None or claim_id is None:
|
||||
return False
|
||||
claim = ClaimSupport(name, claim_id)
|
||||
return claim, decoded_script[op:]
|
||||
|
||||
|
||||
def get_address_from_output_script(script_bytes):
|
||||
output_type = 0
|
||||
decoded = [x for x in script_GetOp(script_bytes)]
|
||||
r = decode_claim_script(decoded)
|
||||
claim_args = None
|
||||
if r is not False:
|
||||
claim_info, decoded = r
|
||||
if isinstance(claim_info, NameClaim):
|
||||
claim_args = (claim_info.name, claim_info.value)
|
||||
output_type |= TYPE_CLAIM
|
||||
elif isinstance(claim_info, ClaimSupport):
|
||||
claim_args = (claim_info.name, claim_info.claim_id)
|
||||
output_type |= TYPE_SUPPORT
|
||||
elif isinstance(claim_info, ClaimUpdate):
|
||||
claim_args = (claim_info.name, claim_info.claim_id, claim_info.value)
|
||||
output_type |= TYPE_UPDATE
|
||||
|
||||
# The Genesis Block, self-payments, and pay-by-IP-address payments look like:
|
||||
# 65 BYTES:... CHECKSIG
|
||||
match_pubkey = [opcodes.OP_PUSHDATA4, opcodes.OP_CHECKSIG]
|
||||
|
||||
# Pay-by-Bitcoin-address TxOuts look like:
|
||||
# DUP HASH160 20 BYTES:... EQUALVERIFY CHECKSIG
|
||||
match_p2pkh = [opcodes.OP_DUP, opcodes.OP_HASH160, opcodes.OP_PUSHDATA4, opcodes.OP_EQUALVERIFY,
|
||||
opcodes.OP_CHECKSIG]
|
||||
|
||||
# p2sh
|
||||
match_p2sh = [opcodes.OP_HASH160, opcodes.OP_PUSHDATA4, opcodes.OP_EQUAL]
|
||||
|
||||
if match_decoded(decoded, match_pubkey):
|
||||
output_val = decoded[0][1].encode('hex')
|
||||
output_type |= TYPE_PUBKEY
|
||||
elif match_decoded(decoded, match_p2pkh):
|
||||
output_val = hash_160_bytes_to_address(decoded[2][1])
|
||||
output_type |= TYPE_ADDRESS
|
||||
elif match_decoded(decoded, match_p2sh):
|
||||
output_val = hash_160_bytes_to_address(decoded[1][1], 5)
|
||||
output_type |= TYPE_ADDRESS
|
||||
else:
|
||||
output_val = bytes
|
||||
output_type |= TYPE_SCRIPT
|
||||
|
||||
if output_type & (TYPE_CLAIM | TYPE_SUPPORT | TYPE_UPDATE):
|
||||
output_val = (claim_args, output_val)
|
||||
|
||||
return output_type, output_val
|
||||
|
||||
|
||||
def parse_input(vds):
|
||||
d = {}
|
||||
prevout_hash = hash_encode(vds.read_bytes(32))
|
||||
prevout_n = vds.read_uint32()
|
||||
scriptSig = vds.read_bytes(vds.read_compact_size())
|
||||
d['scriptSig'] = scriptSig.encode('hex')
|
||||
sequence = vds.read_uint32()
|
||||
if prevout_hash == '00' * 32:
|
||||
d['is_coinbase'] = True
|
||||
else:
|
||||
d['is_coinbase'] = False
|
||||
d['prevout_hash'] = prevout_hash
|
||||
d['prevout_n'] = prevout_n
|
||||
d['sequence'] = sequence
|
||||
d['pubkeys'] = []
|
||||
d['signatures'] = {}
|
||||
d['address'] = None
|
||||
if scriptSig:
|
||||
parse_scriptSig(d, scriptSig)
|
||||
return d
|
||||
|
||||
|
||||
def parse_output(vds, i):
|
||||
d = {}
|
||||
d['value'] = vds.read_int64()
|
||||
scriptPubKey = vds.read_bytes(vds.read_compact_size())
|
||||
d['type'], d['address'] = get_address_from_output_script(scriptPubKey)
|
||||
d['scriptPubKey'] = scriptPubKey.encode('hex')
|
||||
d['prevout_n'] = i
|
||||
return d
|
||||
|
||||
|
||||
def deserialize(raw):
|
||||
vds = BCDataStream()
|
||||
vds.write(raw.decode('hex'))
|
||||
d = {}
|
||||
start = vds.read_cursor
|
||||
d['version'] = vds.read_int32()
|
||||
n_vin = vds.read_compact_size()
|
||||
d['inputs'] = list(parse_input(vds) for i in xrange(n_vin))
|
||||
n_vout = vds.read_compact_size()
|
||||
d['outputs'] = list(parse_output(vds, i) for i in xrange(n_vout))
|
||||
d['lockTime'] = vds.read_uint32()
|
||||
return d
|
||||
|
||||
|
||||
def push_script(x):
|
||||
return op_push(len(x) / 2) + x
|
||||
|
||||
|
||||
class Transaction(object):
|
||||
def __str__(self):
|
||||
if self.raw is None:
|
||||
self.raw = self.serialize()
|
||||
return self.raw
|
||||
|
||||
def __init__(self, raw):
|
||||
if raw is None:
|
||||
self.raw = None
|
||||
elif type(raw) in [str, unicode]:
|
||||
self.raw = raw.strip() if raw else None
|
||||
elif type(raw) is dict:
|
||||
self.raw = raw['hex']
|
||||
def __init__(self, output_or_txid_index, script, sequence=0xFFFFFFFF):
|
||||
if isinstance(output_or_txid_index, Output):
|
||||
self.output = output_or_txid_index # type: Output
|
||||
self.output_txid = self.output.transaction.hash
|
||||
self.output_index = self.output.index
|
||||
else:
|
||||
raise BaseException("cannot initialize transaction", raw)
|
||||
self._inputs = None
|
||||
self._outputs = None
|
||||
self.output = None # type: Output
|
||||
self.output_txid, self.output_index = output_or_txid_index
|
||||
self.sequence = sequence
|
||||
self.is_coinbase = self.output_txid == NULL_HASH
|
||||
self.coinbase = script if self.is_coinbase else None
|
||||
self.script = script if not self.is_coinbase else None # type: InputScript
|
||||
|
||||
def update(self, raw):
|
||||
self.raw = raw
|
||||
self._inputs = None
|
||||
self.deserialize()
|
||||
def link_output(self, output):
|
||||
assert self.output is None
|
||||
assert self.output_txid == output.transaction.id
|
||||
assert self.output_index == output.index
|
||||
self.output = output
|
||||
|
||||
def inputs(self):
|
||||
if self._inputs is None:
|
||||
self.deserialize()
|
||||
return self._inputs
|
||||
@property
|
||||
def amount(self):
|
||||
""" Amount this input adds to the transaction. """
|
||||
if self.output is None:
|
||||
raise ValueError('Cannot get input value without referenced output.')
|
||||
return self.output.amount
|
||||
|
||||
def outputs(self):
|
||||
if self._outputs is None:
|
||||
self.deserialize()
|
||||
return self._outputs
|
||||
@property
|
||||
def effective_amount(self):
|
||||
""" Amount minus fee. """
|
||||
return self.amount - self.fee
|
||||
|
||||
def update_signatures(self, raw):
|
||||
"""Add new signatures to a transaction"""
|
||||
d = deserialize(raw)
|
||||
for i, txin in enumerate(self.inputs()):
|
||||
sigs1 = txin.get('signatures')
|
||||
sigs2 = d['inputs'][i].get('signatures')
|
||||
for sig in sigs2:
|
||||
if sig in sigs1:
|
||||
continue
|
||||
for_sig = Hash(self.tx_for_sig(i).decode('hex'))
|
||||
# der to string
|
||||
order = ecdsa.ecdsa.generator_secp256k1.order()
|
||||
r, s = ecdsa.util.sigdecode_der(sig.decode('hex'), order)
|
||||
sig_string = ecdsa.util.sigencode_string(r, s, order)
|
||||
pubkeys = txin.get('pubkeys')
|
||||
compressed = True
|
||||
for recid in range(4):
|
||||
public_key = MyVerifyingKey.from_signature(sig_string, recid, for_sig,
|
||||
curve=SECP256k1)
|
||||
pubkey = point_to_ser(public_key.pubkey.point, compressed).encode('hex')
|
||||
if pubkey in pubkeys:
|
||||
public_key.verify_digest(sig_string, for_sig,
|
||||
sigdecode=ecdsa.util.sigdecode_string)
|
||||
j = pubkeys.index(pubkey)
|
||||
log.error("adding sig {} {} {} {}".format(i, j, pubkey, sig))
|
||||
self._inputs[i]['signatures'][j] = sig
|
||||
self._inputs[i]['x_pubkeys'][j] = pubkey
|
||||
break
|
||||
# redo raw
|
||||
self.raw = self.serialize()
|
||||
|
||||
def deserialize(self):
|
||||
if self.raw is None:
|
||||
self.raw = self.serialize()
|
||||
if self._inputs is not None:
|
||||
return
|
||||
d = deserialize(self.raw)
|
||||
self._inputs = d['inputs']
|
||||
self._outputs = [(x['type'], x['address'], x['value']) for x in d['outputs']]
|
||||
self.locktime = d['lockTime']
|
||||
return d
|
||||
def __lt__(self, other):
|
||||
return self.effective_amount < other.effective_amount
|
||||
|
||||
@classmethod
|
||||
def from_io(cls, inputs, outputs, locktime=0):
|
||||
self = cls(None)
|
||||
self._inputs = inputs
|
||||
self._outputs = outputs
|
||||
self.locktime = locktime
|
||||
def deserialize_from(cls, stream):
|
||||
txid = stream.read(32)
|
||||
index = stream.read_uint32()
|
||||
script = stream.read_string()
|
||||
sequence = stream.read_uint32()
|
||||
return cls(
|
||||
(txid, index),
|
||||
InputScript(script) if not txid == NULL_HASH else script,
|
||||
sequence
|
||||
)
|
||||
|
||||
def serialize_to(self, stream, alternate_script=None):
|
||||
stream.write(self.output_txid)
|
||||
stream.write_uint32(self.output_index)
|
||||
if alternate_script is not None:
|
||||
stream.write_string(alternate_script)
|
||||
else:
|
||||
if self.is_coinbase:
|
||||
stream.write_string(self.coinbase)
|
||||
else:
|
||||
stream.write_string(self.script.source)
|
||||
stream.write_uint32(self.sequence)
|
||||
|
||||
def to_python_source(self):
|
||||
return (
|
||||
u"InputScript(\n"
|
||||
u" (output_txid=unhexlify('{}'), output_index={}),\n"
|
||||
u" script=unhexlify('{}')\n"
|
||||
u" # tokens: {}\n"
|
||||
u")").format(
|
||||
hexlify(self.output_txid), self.output_index,
|
||||
hexlify(self.coinbase) if self.is_coinbase else hexlify(self.script.source),
|
||||
repr(self.script.tokens)
|
||||
)
|
||||
|
||||
|
||||
class Output(InputOutput):
|
||||
|
||||
def __init__(self, transaction, index, amount, script):
|
||||
self.transaction = transaction # type: Transaction
|
||||
self.index = index # type: int
|
||||
self.amount = amount # type: int
|
||||
self.script = script # type: OutputScript
|
||||
self._effective_amount = None # type: int
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.effective_amount < other.effective_amount
|
||||
|
||||
def _add_and_return(self):
|
||||
self.transaction.add_outputs([self])
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def multisig_script(cls, public_keys, m):
|
||||
n = len(public_keys)
|
||||
assert n <= 15
|
||||
assert m <= n
|
||||
op_m = format(opcodes.OP_1 + m - 1, 'x')
|
||||
op_n = format(opcodes.OP_1 + n - 1, 'x')
|
||||
keylist = [op_push(len(k) / 2) + k for k in public_keys]
|
||||
return op_m + ''.join(keylist) + op_n + 'ae'
|
||||
def pay_pubkey_hash(cls, transaction, index, amount, pubkey_hash):
|
||||
return cls(
|
||||
transaction, index, amount,
|
||||
OutputScript.pay_pubkey_hash(pubkey_hash)
|
||||
)._add_and_return()
|
||||
|
||||
@classmethod
|
||||
def pay_script(cls, output_type, addr):
|
||||
script = ''
|
||||
if output_type & TYPE_CLAIM:
|
||||
claim, addr = addr
|
||||
claim_name, claim_value = claim
|
||||
script += 'b5' # op_claim_name
|
||||
script += push_script(claim_name.encode('hex'))
|
||||
script += push_script(claim_value.encode('hex'))
|
||||
script += '6d75' # op_2drop, op_drop
|
||||
elif output_type & TYPE_SUPPORT:
|
||||
claim, addr = addr
|
||||
claim_name, claim_id = claim
|
||||
script += 'b6'
|
||||
script += push_script(claim_name.encode('hex'))
|
||||
script += push_script(claim_id.encode('hex'))
|
||||
script += '6d75'
|
||||
elif output_type & TYPE_UPDATE:
|
||||
claim, addr = addr
|
||||
claim_name, claim_id, claim_value = claim
|
||||
script += 'b7'
|
||||
script += push_script(claim_name.encode('hex'))
|
||||
script += push_script(claim_id.encode('hex'))
|
||||
script += push_script(claim_value.encode('hex'))
|
||||
script += '6d6d'
|
||||
def pay_claim_name_pubkey_hash(cls, transaction, index, amount, claim_name, claim, pubkey_hash):
|
||||
return cls(
|
||||
transaction, index, amount,
|
||||
OutputScript.pay_claim_name_pubkey_hash(claim_name, claim, pubkey_hash)
|
||||
)._add_and_return()
|
||||
|
||||
if output_type & TYPE_SCRIPT:
|
||||
script += addr.encode('hex')
|
||||
elif output_type & TYPE_ADDRESS: # op_2drop, op_drop
|
||||
addrtype, hash_160 = address_to_hash_160(addr)
|
||||
if addrtype == 0:
|
||||
script += '76a9' # op_dup, op_hash_160
|
||||
script += push_script(hash_160.encode('hex'))
|
||||
script += '88ac' # op_equalverify, op_checksig
|
||||
elif addrtype == 5:
|
||||
script += 'a9' # op_hash_160
|
||||
script += push_script(hash_160.encode('hex'))
|
||||
script += '87' # op_equal
|
||||
else:
|
||||
raise Exception("Unknown address type: %s" % addrtype)
|
||||
else:
|
||||
raise Exception("Unknown output type: %s" % output_type)
|
||||
return script
|
||||
def spend(self, signature=Input.NULL_SIGNATURE, pubkey=Input.NULL_PUBLIC_KEY):
|
||||
""" Create the input to spend this output."""
|
||||
assert self.script.is_pay_pubkey_hash, 'Attempting to spend unsupported output.'
|
||||
script = InputScript.redeem_pubkey_hash(signature, pubkey)
|
||||
return Input(self, script)
|
||||
|
||||
@property
|
||||
def effective_amount(self):
|
||||
""" Amount minus fees it would take to spend this output. """
|
||||
if self._effective_amount is None:
|
||||
txi = self.spend()
|
||||
self._effective_amount = txi.effective_amount
|
||||
return self._effective_amount
|
||||
|
||||
@classmethod
|
||||
def input_script(cls, txin, i, for_sig):
|
||||
# for_sig:
|
||||
# -1 : do not sign, estimate length
|
||||
# i>=0 : serialized tx for signing input i
|
||||
# None : add all known signatures
|
||||
def deserialize_from(cls, stream, transaction, index):
|
||||
return cls(
|
||||
transaction=transaction,
|
||||
index=index,
|
||||
amount=stream.read_uint64(),
|
||||
script=OutputScript(stream.read_string())
|
||||
)
|
||||
|
||||
p2sh = txin.get('redeemScript') is not None
|
||||
num_sig = txin['num_sig'] if p2sh else 1
|
||||
address = txin['address']
|
||||
def serialize_to(self, stream):
|
||||
stream.write_uint64(self.amount)
|
||||
stream.write_string(self.script.source)
|
||||
|
||||
x_signatures = txin['signatures']
|
||||
signatures = filter(None, x_signatures)
|
||||
is_complete = len(signatures) == num_sig
|
||||
def to_python_source(self):
|
||||
return (
|
||||
u"OutputScript(tx, index={}, amount={},\n"
|
||||
u" script=unhexlify('{}')\n"
|
||||
u" # tokens: {}\n"
|
||||
u")").format(
|
||||
self.index, self.amount, hexlify(self.script.source), repr(self.script.tokens))
|
||||
|
||||
if for_sig in [-1, None]:
|
||||
# if we have enough signatures, we use the actual pubkeys
|
||||
# use extended pubkeys (with bip32 derivation)
|
||||
if for_sig == -1:
|
||||
# we assume that signature will be 0x48 bytes long
|
||||
pubkeys = txin['pubkeys']
|
||||
sig_list = ["00" * 0x48] * num_sig
|
||||
elif is_complete:
|
||||
pubkeys = txin['pubkeys']
|
||||
sig_list = ((sig + '01') for sig in signatures)
|
||||
else:
|
||||
pubkeys = txin['x_pubkeys']
|
||||
sig_list = ((sig + '01') if sig else NO_SIGNATURE for sig in x_signatures)
|
||||
script = ''.join(push_script(x) for x in sig_list)
|
||||
if not p2sh:
|
||||
x_pubkey = pubkeys[0]
|
||||
if x_pubkey is None:
|
||||
addrtype, h160 = address_to_hash_160(txin['address'])
|
||||
x_pubkey = 'fd' + (chr(addrtype) + h160).encode('hex')
|
||||
script += push_script(x_pubkey)
|
||||
else:
|
||||
script = '00' + script # put op_0 in front of script
|
||||
redeem_script = cls.multisig_script(pubkeys, num_sig)
|
||||
script += push_script(redeem_script)
|
||||
|
||||
elif for_sig == i:
|
||||
script_type = TYPE_ADDRESS
|
||||
if 'is_claim' in txin and txin['is_claim']:
|
||||
script_type |= TYPE_CLAIM
|
||||
address = ((txin['claim_name'], txin['claim_value']), address)
|
||||
elif 'is_support' in txin and txin['is_support']:
|
||||
script_type |= TYPE_SUPPORT
|
||||
address = ((txin['claim_name'], txin['claim_id']), address)
|
||||
elif 'is_update' in txin and txin['is_update']:
|
||||
script_type |= TYPE_UPDATE
|
||||
address = ((txin['claim_name'], txin['claim_id'], txin['claim_value']), address)
|
||||
script = txin['redeemScript'] if p2sh else cls.pay_script(script_type, address)
|
||||
else:
|
||||
script = ''
|
||||
class Transaction:
|
||||
|
||||
return script
|
||||
def __init__(self, raw=None, version=1, locktime=0, height=None, is_saved=False):
|
||||
self._raw = raw
|
||||
self._hash = None
|
||||
self._id = None
|
||||
self.version = version # type: int
|
||||
self.locktime = locktime # type: int
|
||||
self.height = height # type: int
|
||||
self.inputs = [] # type: List[Input]
|
||||
self.outputs = [] # type: List[Output]
|
||||
self.is_saved = is_saved # type: bool
|
||||
if raw is not None:
|
||||
self._deserialize()
|
||||
|
||||
@classmethod
|
||||
def serialize_input(cls, txin, i, for_sig):
|
||||
# Prev hash and index
|
||||
s = txin['prevout_hash'].decode('hex')[::-1].encode('hex')
|
||||
s += int_to_hex(txin['prevout_n'], 4)
|
||||
# Script length, script, sequence
|
||||
script = cls.input_script(txin, i, for_sig)
|
||||
s += var_int(len(script) / 2)
|
||||
s += script
|
||||
s += "ffffffff"
|
||||
return s
|
||||
|
||||
def BIP_LI01_sort(self):
|
||||
# See https://github.com/kristovatlas/rfc/blob/master/bips/bip-li01.mediawiki
|
||||
self._inputs.sort(key=lambda i: (i['prevout_hash'], i['prevout_n']))
|
||||
self._outputs.sort(key=lambda o: (o[2], self.pay_script(o[0], o[1])))
|
||||
|
||||
def serialize(self, for_sig=None):
|
||||
inputs = self.inputs()
|
||||
outputs = self.outputs()
|
||||
s = int_to_hex(1, 4) # version
|
||||
s += var_int(len(inputs)) # number of inputs
|
||||
for i, txin in enumerate(inputs):
|
||||
s += self.serialize_input(txin, i, for_sig)
|
||||
s += var_int(len(outputs)) # number of outputs
|
||||
for output in outputs:
|
||||
output_type, addr, amount = output
|
||||
s += int_to_hex(amount, 8) # amount
|
||||
script = self.pay_script(output_type, addr)
|
||||
s += var_int(len(script) / 2) # script length
|
||||
s += script # script
|
||||
s += int_to_hex(0, 4) # lock time
|
||||
if for_sig is not None and for_sig != -1:
|
||||
s += int_to_hex(1, 4) # hash type
|
||||
return s
|
||||
|
||||
def tx_for_sig(self, i):
|
||||
return self.serialize(for_sig=i)
|
||||
@property
|
||||
def id(self):
|
||||
if self._id is None:
|
||||
self._id = self.hash[::-1]
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def hash(self):
|
||||
return Hash(self.raw.decode('hex'))[::-1].encode('hex')
|
||||
if self._hash is None:
|
||||
self._hash = sha256(sha256(self.raw))
|
||||
return self._hash
|
||||
|
||||
def get_claim_id(self, nout):
|
||||
if nout < 0:
|
||||
raise IndexError
|
||||
if not self._outputs[nout][0] & TYPE_CLAIM:
|
||||
raise ValueError
|
||||
tx_hash = rev_hex(self.hash()).decode('hex')
|
||||
return encode_claim_id_hex(claim_id_hash(tx_hash, nout))
|
||||
@property
|
||||
def raw(self):
|
||||
if self._raw is None:
|
||||
self._raw = self._serialize()
|
||||
return self._raw
|
||||
|
||||
def add_inputs(self, inputs):
|
||||
self._inputs.extend(inputs)
|
||||
self.raw = None
|
||||
def _reset(self):
|
||||
self._raw = None
|
||||
self._hash = None
|
||||
self._id = None
|
||||
|
||||
def add_outputs(self, outputs):
|
||||
self._outputs.extend(outputs)
|
||||
self.raw = None
|
||||
|
||||
def input_value(self):
|
||||
return sum(x['value'] for x in self.inputs())
|
||||
|
||||
def output_value(self):
|
||||
return sum(val for tp, addr, val in self.outputs())
|
||||
|
||||
def get_fee(self):
|
||||
return self.input_value() - self.output_value()
|
||||
|
||||
def is_final(self):
|
||||
return not any([x.get('sequence') < 0xffffffff - 1 for x in self.inputs()])
|
||||
|
||||
@classmethod
|
||||
def fee_for_size(cls, relay_fee, fee_per_kb, size):
|
||||
'''Given a fee per kB in satoshis, and a tx size in bytes,
|
||||
returns the transaction fee.'''
|
||||
fee = int(fee_per_kb * size / 1000.)
|
||||
if fee < relay_fee:
|
||||
fee = relay_fee
|
||||
return fee
|
||||
|
||||
def estimated_size(self):
|
||||
'''Return an estimated tx size in bytes.'''
|
||||
return len(self.serialize(-1)) / 2 # ASCII hex string
|
||||
|
||||
@classmethod
|
||||
def estimated_input_size(cls, txin):
|
||||
'''Return an estimated of serialized input size in bytes.'''
|
||||
return len(cls.serialize_input(txin, -1, -1)) / 2
|
||||
|
||||
def estimated_fee(self, relay_fee, fee_per_kb):
|
||||
'''Return an estimated fee given a fee per kB in satoshis.'''
|
||||
return self.fee_for_size(relay_fee, fee_per_kb, self.estimated_size())
|
||||
|
||||
def signature_count(self):
|
||||
r = 0
|
||||
s = 0
|
||||
for txin in self.inputs():
|
||||
if txin.get('is_coinbase'):
|
||||
continue
|
||||
signatures = filter(None, txin.get('signatures', []))
|
||||
s += len(signatures)
|
||||
r += txin.get('num_sig', -1)
|
||||
return s, r
|
||||
def get_claim_id(self, output_index):
|
||||
script = self.outputs[output_index]
|
||||
assert script.script.is_claim_name(), 'Not a name claim.'
|
||||
return claim_id_hash(self.hash, output_index)
|
||||
|
||||
@property
|
||||
def is_complete(self):
|
||||
s, r = self.signature_count()
|
||||
return r == s
|
||||
|
||||
def inputs_without_script(self):
|
||||
out = set()
|
||||
for i, txin in enumerate(self.inputs()):
|
||||
if txin.get('scriptSig') == '':
|
||||
out.add(i)
|
||||
return out
|
||||
@property
|
||||
def fee(self):
|
||||
""" Fee that will actually be paid."""
|
||||
return self.input_sum - self.output_sum
|
||||
|
||||
def inputs_to_sign(self):
|
||||
out = set()
|
||||
for txin in self.inputs():
|
||||
num_sig = txin.get('num_sig')
|
||||
if num_sig is None:
|
||||
continue
|
||||
x_signatures = txin['signatures']
|
||||
signatures = filter(None, x_signatures)
|
||||
if len(signatures) == num_sig:
|
||||
# input is complete
|
||||
continue
|
||||
for k, x_pubkey in enumerate(txin['x_pubkeys']):
|
||||
if x_signatures[k] is not None:
|
||||
# this pubkey already signed
|
||||
continue
|
||||
out.add(x_pubkey)
|
||||
return out
|
||||
@property
|
||||
def size(self):
|
||||
""" Size in bytes of the entire transaction. """
|
||||
return len(self.raw)
|
||||
|
||||
def sign(self, keypairs):
|
||||
for i, txin in enumerate(self.inputs()):
|
||||
num = txin['num_sig']
|
||||
for x_pubkey in txin['x_pubkeys']:
|
||||
signatures = filter(None, txin['signatures'])
|
||||
if len(signatures) == num:
|
||||
# txin is complete
|
||||
break
|
||||
if x_pubkey in keypairs.keys():
|
||||
log.debug("adding signature for %s", x_pubkey)
|
||||
# add pubkey to txin
|
||||
txin = self._inputs[i]
|
||||
x_pubkeys = txin['x_pubkeys']
|
||||
ii = x_pubkeys.index(x_pubkey)
|
||||
sec = keypairs[x_pubkey]
|
||||
pubkey = public_key_from_private_key(sec)
|
||||
txin['x_pubkeys'][ii] = pubkey
|
||||
txin['pubkeys'][ii] = pubkey
|
||||
self._inputs[i] = txin
|
||||
# add signature
|
||||
for_sig = Hash(self.tx_for_sig(i).decode('hex'))
|
||||
pkey = regenerate_key(sec)
|
||||
secexp = pkey.secret
|
||||
private_key = MySigningKey.from_secret_exponent(secexp, curve=SECP256k1)
|
||||
public_key = private_key.get_verifying_key()
|
||||
sig = private_key.sign_digest_deterministic(for_sig, hashfunc=hashlib.sha256,
|
||||
sigencode=ecdsa.util.sigencode_der)
|
||||
assert public_key.verify_digest(sig, for_sig,
|
||||
sigdecode=ecdsa.util.sigdecode_der)
|
||||
txin['signatures'][ii] = sig.encode('hex')
|
||||
self._inputs[i] = txin
|
||||
log.debug("is_complete: %s", self.is_complete())
|
||||
self.raw = self.serialize()
|
||||
@property
|
||||
def base_size(self):
|
||||
""" Size in bytes of transaction meta data and all outputs; without inputs. """
|
||||
return len(self._serialize(with_inputs=False))
|
||||
|
||||
def get_outputs(self):
|
||||
"""convert pubkeys to addresses"""
|
||||
o = []
|
||||
for type, x, v in self.outputs():
|
||||
if type & (TYPE_CLAIM | TYPE_UPDATE | TYPE_SUPPORT):
|
||||
x = x[1]
|
||||
if type & TYPE_ADDRESS:
|
||||
addr = x
|
||||
elif type & TYPE_PUBKEY:
|
||||
addr = public_key_to_address(x.decode('hex'))
|
||||
@property
|
||||
def base_fee(self):
|
||||
""" Fee for the transaction header and all outputs; without inputs. """
|
||||
byte_fee = get_wallet_manager().fee_per_byte * self.base_size
|
||||
return max(byte_fee, self.claim_name_fee)
|
||||
|
||||
@property
|
||||
def claim_name_fee(self):
|
||||
char_fee = get_wallet_manager().fee_per_name_char
|
||||
fee = 0
|
||||
for output in self.outputs:
|
||||
if output.script.is_claim_name:
|
||||
fee += len(output.script.values['claim_name']) * char_fee
|
||||
return fee
|
||||
|
||||
def _serialize(self, with_inputs=True):
|
||||
stream = BCDataStream()
|
||||
stream.write_uint32(self.version)
|
||||
if with_inputs:
|
||||
stream.write_compact_size(len(self.inputs))
|
||||
for txin in self.inputs:
|
||||
txin.serialize_to(stream)
|
||||
stream.write_compact_size(len(self.outputs))
|
||||
for txout in self.outputs:
|
||||
txout.serialize_to(stream)
|
||||
stream.write_uint32(self.locktime)
|
||||
return stream.get_bytes()
|
||||
|
||||
def _serialize_for_signature(self, signing_input):
|
||||
stream = BCDataStream()
|
||||
stream.write_uint32(self.version)
|
||||
stream.write_compact_size(len(self.inputs))
|
||||
for i, txin in enumerate(self.inputs):
|
||||
if signing_input == i:
|
||||
txin.serialize_to(stream, txin.output.script.source)
|
||||
else:
|
||||
addr = 'SCRIPT ' + x.encode('hex')
|
||||
o.append((addr, v)) # consider using yield (addr, v)
|
||||
return o
|
||||
txin.serialize_to(stream, b'')
|
||||
stream.write_compact_size(len(self.outputs))
|
||||
for txout in self.outputs:
|
||||
txout.serialize_to(stream)
|
||||
stream.write_uint32(self.locktime)
|
||||
stream.write_uint32(1) # signature hash type: SIGHASH_ALL
|
||||
return stream.get_bytes()
|
||||
|
||||
def get_output_addresses(self):
|
||||
return [addr for addr, val in self.get_outputs()]
|
||||
def _deserialize(self):
|
||||
if self._raw is not None:
|
||||
stream = BCDataStream(self._raw)
|
||||
self.version = stream.read_uint32()
|
||||
input_count = stream.read_compact_size()
|
||||
self.inputs = [Input.deserialize_from(stream) for _ in range(input_count)]
|
||||
output_count = stream.read_compact_size()
|
||||
self.outputs = [Output.deserialize_from(stream, self, i) for i in range(output_count)]
|
||||
self.locktime = stream.read_uint32()
|
||||
|
||||
def has_address(self, addr):
|
||||
return (addr in self.get_output_addresses()) or (
|
||||
addr in (tx.get("address") for tx in self.inputs()))
|
||||
def add_inputs(self, inputs):
|
||||
self.inputs.extend(inputs)
|
||||
self._reset()
|
||||
|
||||
def as_dict(self):
|
||||
if self.raw is None:
|
||||
self.raw = self.serialize()
|
||||
self.deserialize()
|
||||
out = {
|
||||
'hex': self.raw,
|
||||
'complete': self.is_complete()
|
||||
}
|
||||
return out
|
||||
def add_outputs(self, outputs):
|
||||
self.outputs.extend(outputs)
|
||||
self._reset()
|
||||
|
||||
def requires_fee(self, wallet):
|
||||
# see https://en.bitcoin.it/wiki/Transaction_fees
|
||||
#
|
||||
# size must be smaller than 1 kbyte for free tx
|
||||
size = len(self.serialize(-1)) / 2
|
||||
if size >= 10000:
|
||||
return True
|
||||
# all outputs must be 0.01 BTC or larger for free tx
|
||||
for addr, value in self.get_outputs():
|
||||
if value < 1000000:
|
||||
return True
|
||||
# priority must be large enough for free tx
|
||||
threshold = 57600000
|
||||
weight = 0
|
||||
for txin in self.inputs():
|
||||
age = wallet.get_confirmations(txin["prevout_hash"])[0]
|
||||
weight += txin["value"] * age
|
||||
priority = weight / size
|
||||
log.error("{} {}".format(priority, threshold))
|
||||
def sign(self, wallet): # type: (Wallet) -> bool
|
||||
for i, txi in enumerate(self.inputs):
|
||||
txo_script = txi.output.script
|
||||
if txo_script.is_pay_pubkey_hash:
|
||||
address = hash160_to_address(txo_script.values['pubkey_hash'], wallet.chain)
|
||||
private_key = wallet.get_private_key_for_address(address)
|
||||
tx = self._serialize_for_signature(i)
|
||||
txi.script.values['signature'] = private_key.sign(tx)+six.int2byte(1)
|
||||
txi.script.values['pubkey'] = private_key.public_key.pubkey_bytes
|
||||
txi.script.generate()
|
||||
self._reset()
|
||||
return True
|
||||
|
||||
return priority < threshold
|
||||
def sort(self):
|
||||
# See https://github.com/kristovatlas/rfc/blob/master/bips/bip-li01.mediawiki
|
||||
self.inputs.sort(key=lambda i: (i['prevout_hash'], i['prevout_n']))
|
||||
self.outputs.sort(key=lambda o: (o[2], pay_script(o[0], o[1])))
|
||||
|
||||
@property
|
||||
def input_sum(self):
|
||||
return sum(i.amount for i in self.inputs)
|
||||
|
||||
@property
|
||||
def output_sum(self):
|
||||
return sum(o.amount for o in self.outputs)
|
||||
|
||||
def to_python_source(self):
|
||||
s = io.StringIO()
|
||||
s.write(u'tx = Transaction(version={}, locktime={}, height={})\n'.format(
|
||||
self.version, self.locktime, self.height
|
||||
))
|
||||
for txi in self.inputs:
|
||||
s.write(u'tx.add_input(')
|
||||
s.write(txi.to_python_source())
|
||||
s.write(u')\n')
|
||||
for txo in self.outputs:
|
||||
s.write(u'tx.add_output(')
|
||||
s.write(txo.to_python_source())
|
||||
s.write(u')\n')
|
||||
s.write(u'# tx.id: unhexlify("{}")\n'.format(hexlify(self.id)))
|
||||
s.write(u'# tx.raw: unhexlify("{}")\n'.format(hexlify(self.raw)))
|
||||
return s.getvalue()
|
||||
|
|
|
@ -1,70 +1,32 @@
|
|||
import logging
|
||||
import os
|
||||
import re
|
||||
from decimal import Decimal
|
||||
import json
|
||||
from .constants import NO_SIGNATURE
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
from binascii import unhexlify, hexlify
|
||||
|
||||
|
||||
def subclass_tuple(name, base):
|
||||
return type(name, (base,), {'__slots__': ()})
|
||||
|
||||
|
||||
def normalize_version(v):
|
||||
return [int(x) for x in re.sub(r'(\.0+)*$', '', v).split(".")]
|
||||
class cachedproperty(object):
|
||||
|
||||
def __init__(self, f):
|
||||
self.f = f
|
||||
|
||||
def __get__(self, obj, type):
|
||||
obj = obj or type
|
||||
value = self.f(obj)
|
||||
setattr(obj, self.f.__name__, value)
|
||||
return value
|
||||
|
||||
|
||||
def json_decode(x):
|
||||
try:
|
||||
return json.loads(x, parse_float=Decimal)
|
||||
except:
|
||||
return x
|
||||
def bytes_to_int(be_bytes):
|
||||
""" Interprets a big-endian sequence of bytes as an integer. """
|
||||
return int(hexlify(be_bytes), 16)
|
||||
|
||||
|
||||
def user_dir():
|
||||
if "HOME" in os.environ:
|
||||
return os.path.join(os.environ["HOME"], ".lbryum")
|
||||
elif "APPDATA" in os.environ:
|
||||
return os.path.join(os.environ["APPDATA"], "LBRYum")
|
||||
elif "LOCALAPPDATA" in os.environ:
|
||||
return os.path.join(os.environ["LOCALAPPDATA"], "LBRYum")
|
||||
elif 'ANDROID_DATA' in os.environ:
|
||||
try:
|
||||
import jnius
|
||||
env = jnius.autoclass('android.os.Environment')
|
||||
_dir = env.getExternalStorageDirectory().getPath()
|
||||
return _dir + '/lbryum/'
|
||||
except ImportError:
|
||||
pass
|
||||
return "/sdcard/lbryum/"
|
||||
else:
|
||||
# raise Exception("No home directory found in environment variables.")
|
||||
return
|
||||
|
||||
|
||||
def format_satoshis(x, is_diff=False, num_zeros=0, decimal_point=8, whitespaces=False):
|
||||
from locale import localeconv
|
||||
if x is None:
|
||||
return 'unknown'
|
||||
x = int(x) # Some callers pass Decimal
|
||||
scale_factor = pow(10, decimal_point)
|
||||
integer_part = "{:n}".format(int(abs(x) / scale_factor))
|
||||
if x < 0:
|
||||
integer_part = '-' + integer_part
|
||||
elif is_diff:
|
||||
integer_part = '+' + integer_part
|
||||
dp = localeconv()['decimal_point']
|
||||
fract_part = ("{:0" + str(decimal_point) + "}").format(abs(x) % scale_factor)
|
||||
fract_part = fract_part.rstrip('0')
|
||||
if len(fract_part) < num_zeros:
|
||||
fract_part += "0" * (num_zeros - len(fract_part))
|
||||
result = integer_part + dp + fract_part
|
||||
if whitespaces:
|
||||
result += " " * (decimal_point - len(fract_part))
|
||||
result = " " * (15 - len(result)) + result
|
||||
return result.decode('utf8')
|
||||
def int_to_bytes(value):
|
||||
""" Converts an integer to a big-endian sequence of bytes. """
|
||||
length = (value.bit_length() + 7) // 8
|
||||
h = '%x' % value
|
||||
return unhexlify(('0' * (len(h) % 2) + h).zfill(length * 2))
|
||||
|
||||
|
||||
def rev_hex(s):
|
||||
|
@ -81,41 +43,5 @@ def hex_to_int(s):
|
|||
return int('0x' + s[::-1].encode('hex'), 16)
|
||||
|
||||
|
||||
def var_int(i):
|
||||
# https://en.bitcoin.it/wiki/Protocol_specification#Variable_length_integer
|
||||
if i < 0xfd:
|
||||
return int_to_hex(i)
|
||||
elif i <= 0xffff:
|
||||
return "fd" + int_to_hex(i, 2)
|
||||
elif i <= 0xffffffff:
|
||||
return "fe" + int_to_hex(i, 4)
|
||||
else:
|
||||
return "ff" + int_to_hex(i, 8)
|
||||
|
||||
|
||||
# This function comes from bitcointools, bct-LICENSE.txt.
|
||||
def long_hex(bytes):
|
||||
return bytes.encode('hex_codec')
|
||||
|
||||
|
||||
# This function comes from bitcointools, bct-LICENSE.txt.
|
||||
def short_hex(bytes):
|
||||
t = bytes.encode('hex_codec')
|
||||
if len(t) < 11:
|
||||
return t
|
||||
return t[0:4] + "..." + t[-4:]
|
||||
|
||||
|
||||
def parse_sig(x_sig):
|
||||
s = []
|
||||
for sig in x_sig:
|
||||
if sig[-2:] == '01':
|
||||
s.append(sig[:-2])
|
||||
else:
|
||||
assert sig == NO_SIGNATURE
|
||||
s.append(None)
|
||||
return s
|
||||
|
||||
|
||||
def is_extended_pubkey(x_pubkey):
|
||||
return x_pubkey[0:2] in ['fe', 'ff']
|
||||
def hash_encode(x):
|
||||
return x[::-1].encode('hex')
|
||||
|
|
|
@ -1,72 +1,114 @@
|
|||
import copy
|
||||
import stat
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
|
||||
from .constants import NEW_SEED_VERSION
|
||||
from .account import Account
|
||||
from .mnemonic import Mnemonic
|
||||
from .lbrycrd import pw_encode, bip32_private_derivation, bip32_root
|
||||
from .blockchain import BlockchainTransactions
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
from lbrynet.wallet.account import Account
|
||||
from lbrynet.wallet.constants import MAIN_CHAIN
|
||||
|
||||
|
||||
class WalletStorage:
|
||||
class Wallet:
|
||||
|
||||
def __init__(self, path):
|
||||
self.data = {}
|
||||
self.path = path
|
||||
self.file_exists = False
|
||||
self.modified = False
|
||||
self.path and self.read()
|
||||
def __init__(self, **kwargs):
|
||||
self.name = kwargs.get('name', 'Wallet')
|
||||
self.chain = kwargs.get('chain', MAIN_CHAIN)
|
||||
self.accounts = kwargs.get('accounts') or {0: Account.generate()}
|
||||
|
||||
def read(self):
|
||||
try:
|
||||
with open(self.path, "r") as f:
|
||||
data = f.read()
|
||||
except IOError:
|
||||
return
|
||||
try:
|
||||
self.data = json.loads(data)
|
||||
except Exception:
|
||||
self.data = {}
|
||||
raise IOError("Cannot read wallet file '%s'" % self.path)
|
||||
self.file_exists = True
|
||||
@classmethod
|
||||
def from_json(cls, json_data):
|
||||
if 'accounts' in json_data:
|
||||
json_data = json_data.copy()
|
||||
json_data['accounts'] = {
|
||||
a_id: Account.from_json(a) for
|
||||
a_id, a in json_data['accounts'].items()
|
||||
}
|
||||
return cls(**json_data)
|
||||
|
||||
def get(self, key, default=None):
|
||||
v = self.data.get(key)
|
||||
if v is None:
|
||||
v = default
|
||||
def to_json(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'chain': self.chain,
|
||||
'accounts': {
|
||||
a_id: a.to_json() for
|
||||
a_id, a in self.accounts.items()
|
||||
}
|
||||
}
|
||||
|
||||
@property
|
||||
def default_account(self):
|
||||
return self.accounts.get(0, None)
|
||||
|
||||
@property
|
||||
def addresses(self):
|
||||
for account in self.accounts.values():
|
||||
for address in account.addresses:
|
||||
yield address
|
||||
|
||||
def ensure_enough_addresses(self):
|
||||
return [
|
||||
address
|
||||
for account in self.accounts.values()
|
||||
for address in account.ensure_enough_addresses()
|
||||
]
|
||||
|
||||
def get_private_key_for_address(self, address):
|
||||
for account in self.accounts.values():
|
||||
private_key = account.get_private_key_for_address(address)
|
||||
if private_key is not None:
|
||||
return private_key
|
||||
|
||||
|
||||
class EphemeralWalletStorage(dict):
|
||||
|
||||
LATEST_VERSION = 2
|
||||
|
||||
def save(self):
|
||||
return json.dumps(self, indent=4, sort_keys=True)
|
||||
|
||||
def upgrade(self):
|
||||
|
||||
def _rename_property(old, new):
|
||||
if old in self:
|
||||
old_value = self[old]
|
||||
del self[old]
|
||||
if new not in self:
|
||||
self[new] = old_value
|
||||
|
||||
if self.get('version', 1) == 1: # upgrade from version 1 to version 2
|
||||
# TODO: `addr_history` should actually be imported into SQLStorage and removed from wallet.
|
||||
_rename_property('addr_history', 'history')
|
||||
_rename_property('use_encryption', 'encrypted')
|
||||
_rename_property('gap_limit', 'gap_limit_for_receiving')
|
||||
self['version'] = 2
|
||||
|
||||
self.save()
|
||||
|
||||
|
||||
class PermanentWalletStorage(EphemeralWalletStorage):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PermanentWalletStorage, self).__init__(*args, **kwargs)
|
||||
self.path = None
|
||||
|
||||
@classmethod
|
||||
def from_path(cls, path):
|
||||
if os.path.exists(path):
|
||||
with open(path, "r") as f:
|
||||
json_data = f.read()
|
||||
json_dict = json.loads(json_data)
|
||||
storage = cls(**json_dict)
|
||||
if 'version' in storage and storage['version'] != storage.LATEST_VERSION:
|
||||
storage.upgrade()
|
||||
else:
|
||||
v = copy.deepcopy(v)
|
||||
return v
|
||||
storage = cls()
|
||||
storage.path = path
|
||||
return storage
|
||||
|
||||
def put(self, key, value):
|
||||
try:
|
||||
json.dumps(key)
|
||||
json.dumps(value)
|
||||
except:
|
||||
return
|
||||
if value is not None:
|
||||
if self.data.get(key) != value:
|
||||
self.modified = True
|
||||
self.data[key] = copy.deepcopy(value)
|
||||
elif key in self.data:
|
||||
self.modified = True
|
||||
self.data.pop(key)
|
||||
def save(self):
|
||||
json_data = super(PermanentWalletStorage, self).save()
|
||||
|
||||
def write(self):
|
||||
self._write()
|
||||
|
||||
def _write(self):
|
||||
if not self.modified:
|
||||
return
|
||||
s = json.dumps(self.data, indent=4, sort_keys=True)
|
||||
temp_path = "%s.tmp.%s" % (self.path, os.getpid())
|
||||
with open(temp_path, "w") as f:
|
||||
f.write(s)
|
||||
f.write(json_data)
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
|
||||
|
@ -74,169 +116,12 @@ class WalletStorage:
|
|||
mode = os.stat(self.path).st_mode
|
||||
else:
|
||||
mode = stat.S_IREAD | stat.S_IWRITE
|
||||
# perform atomic write on POSIX systems
|
||||
|
||||
try:
|
||||
os.rename(temp_path, self.path)
|
||||
except:
|
||||
os.remove(self.path)
|
||||
os.rename(temp_path, self.path)
|
||||
os.chmod(self.path, mode)
|
||||
self.modified = False
|
||||
|
||||
def upgrade(self):
|
||||
|
||||
def _rename_property(old, new):
|
||||
if old in self.data:
|
||||
old_value = self.data[old]
|
||||
del self.data[old]
|
||||
if new not in self.data:
|
||||
self.data[new] = old_value
|
||||
|
||||
_rename_property('addr_history', 'history')
|
||||
_rename_property('use_encryption', 'encrypted')
|
||||
|
||||
|
||||
class Wallet:
|
||||
|
||||
root_name = 'x/'
|
||||
root_derivation = 'm/'
|
||||
gap_limit_for_change = 6
|
||||
|
||||
def __init__(self, path, headers):
|
||||
self.storage = storage = WalletStorage(path)
|
||||
storage.upgrade()
|
||||
self.headers = headers
|
||||
self.accounts = self._instantiate_accounts(storage.get('accounts', {}))
|
||||
self.history = BlockchainTransactions(storage.get('history', {}))
|
||||
self.master_public_keys = storage.get('master_public_keys', {})
|
||||
self.master_private_keys = storage.get('master_private_keys', {})
|
||||
self.gap_limit = storage.get('gap_limit', 20)
|
||||
self.seed = storage.get('seed', '')
|
||||
self.seed_version = storage.get('seed_version', NEW_SEED_VERSION)
|
||||
self.encrypted = storage.get('encrypted', storage.get('use_encryption', False))
|
||||
self.claim_certificates = storage.get('claim_certificates', {})
|
||||
self.default_certificate_claim = storage.get('default_certificate_claim', None)
|
||||
|
||||
def _instantiate_accounts(self, accounts):
|
||||
instances = {}
|
||||
for index, details in accounts.items():
|
||||
if 'xpub' in details:
|
||||
instances[index] = Account(
|
||||
details, self.gap_limit, self.gap_limit_for_change, self.is_address_old
|
||||
)
|
||||
else:
|
||||
log.error("cannot load account: {}".format(details))
|
||||
return instances
|
||||
|
||||
@property
|
||||
def exists(self):
|
||||
return self.storage.file_exists
|
||||
|
||||
@property
|
||||
def default_account(self):
|
||||
return self.accounts['0']
|
||||
|
||||
@property
|
||||
def sequences(self):
|
||||
for account in self.accounts.values():
|
||||
for sequence in account.sequences:
|
||||
yield sequence
|
||||
|
||||
@property
|
||||
def addresses(self):
|
||||
for sequence in self.sequences:
|
||||
for address in sequence.addresses:
|
||||
yield address
|
||||
|
||||
@property
|
||||
def receiving_addresses(self):
|
||||
for account in self.accounts.values():
|
||||
for address in account.receiving.addresses:
|
||||
yield address
|
||||
|
||||
@property
|
||||
def change_addresses(self):
|
||||
for account in self.accounts.values():
|
||||
for address in account.receiving.addresses:
|
||||
yield address
|
||||
|
||||
@property
|
||||
def addresses_without_history(self):
|
||||
for address in self.addresses:
|
||||
if not self.history.has_address(address):
|
||||
yield address
|
||||
|
||||
def ensure_enough_addresses(self):
|
||||
return [
|
||||
address
|
||||
for sequence in self.sequences
|
||||
for address in sequence.ensure_enough_addresses()
|
||||
]
|
||||
|
||||
def create(self):
|
||||
mnemonic = Mnemonic(self.storage.get('lang', 'eng'))
|
||||
seed = mnemonic.make_seed()
|
||||
self.add_seed(seed, None)
|
||||
self.add_xprv_from_seed(seed, self.root_name, None)
|
||||
account = Account(
|
||||
{'xpub': self.master_public_keys.get("x/")},
|
||||
self.gap_limit,
|
||||
self.gap_limit_for_change,
|
||||
self.is_address_old
|
||||
)
|
||||
self.add_account('0', account)
|
||||
|
||||
def add_seed(self, seed, password):
|
||||
if self.seed:
|
||||
raise Exception("a seed exists")
|
||||
self.seed_version, self.seed = self.format_seed(seed)
|
||||
if password:
|
||||
self.seed = pw_encode(self.seed, password)
|
||||
self.storage.put('seed', self.seed)
|
||||
self.storage.put('seed_version', self.seed_version)
|
||||
self.set_use_encryption(password is not None)
|
||||
|
||||
@staticmethod
|
||||
def format_seed(seed):
|
||||
return NEW_SEED_VERSION, ' '.join(seed.split())
|
||||
|
||||
def add_xprv_from_seed(self, seed, name, password, passphrase=''):
|
||||
xprv, xpub = bip32_root(Mnemonic.mnemonic_to_seed(seed, passphrase))
|
||||
xprv, xpub = bip32_private_derivation(xprv, "m/", self.root_derivation)
|
||||
self.add_master_public_key(name, xpub)
|
||||
self.add_master_private_key(name, xprv, password)
|
||||
|
||||
def add_master_public_key(self, name, xpub):
|
||||
if xpub in self.master_public_keys.values():
|
||||
raise BaseException('Duplicate master public key')
|
||||
self.master_public_keys[name] = xpub
|
||||
self.storage.put('master_public_keys', self.master_public_keys)
|
||||
|
||||
def add_master_private_key(self, name, xpriv, password):
|
||||
self.master_private_keys[name] = pw_encode(xpriv, password)
|
||||
self.storage.put('master_private_keys', self.master_private_keys)
|
||||
|
||||
def add_account(self, account_id, account):
|
||||
self.accounts[account_id] = account
|
||||
self.save_accounts()
|
||||
|
||||
def set_use_encryption(self, use_encryption):
|
||||
self.use_encryption = use_encryption
|
||||
self.storage.put('use_encryption', use_encryption)
|
||||
|
||||
def save_accounts(self):
|
||||
d = {}
|
||||
for k, v in self.accounts.items():
|
||||
d[k] = v.as_dict()
|
||||
self.storage.put('accounts', d)
|
||||
|
||||
def is_address_old(self, address, age_limit=2):
|
||||
age = -1
|
||||
for tx in self.history.get_transactions(address, []):
|
||||
if tx.height == 0:
|
||||
tx_age = 0
|
||||
else:
|
||||
tx_age = self.headers.height - tx.height + 1
|
||||
if tx_age > age:
|
||||
age = tx_age
|
||||
return age > age_limit
|
||||
return json_data
|
||||
|
|
|
@ -27,3 +27,4 @@ txJSON-RPC==0.5
|
|||
wsgiref==0.1.2
|
||||
zope.interface==4.3.3
|
||||
treq==17.8.0
|
||||
typing
|
||||
|
|
Loading…
Add table
Reference in a new issue