finally generating fully signed tx submittable to lbrycrd

This commit is contained in:
Lex Berezhny 2018-04-22 22:23:42 -04:00 committed by Jack Robison
parent dcd8a6bb0e
commit 83958604d5
No known key found for this signature in database
GPG key ID: DF25C68FE0239BB2
25 changed files with 1678 additions and 1973 deletions

View 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()

View file

@ -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'

View 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()

View 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()
)

View file

@ -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

View file

@ -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
View 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))

View file

@ -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

View file

@ -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,

View file

@ -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
View 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)

View file

@ -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()

View file

@ -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)

View file

@ -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, \

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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()

View file

@ -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+')

View file

@ -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()

View file

@ -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()

View file

@ -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')

View file

@ -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

View file

@ -27,3 +27,4 @@ txJSON-RPC==0.5
wsgiref==0.1.2
zope.interface==4.3.3
treq==17.8.0
typing