wip: implementation is now generic and supports multiple currencies

This commit is contained in:
Lex Berezhny 2018-04-30 03:04:52 -04:00 committed by Jack Robison
parent 83958604d5
commit 5e71dcbaf0
No known key found for this signature in database
GPG key ID: DF25C68FE0239BB2
29 changed files with 1214 additions and 797 deletions

View file

@ -29,6 +29,7 @@ ENV_NAMESPACE = 'LBRY_'
LBRYCRD_WALLET = 'lbrycrd' LBRYCRD_WALLET = 'lbrycrd'
LBRYUM_WALLET = 'lbryum' LBRYUM_WALLET = 'lbryum'
PTC_WALLET = 'ptc' PTC_WALLET = 'ptc'
TORBA_WALLET = 'torba'
PROTOCOL_PREFIX = 'lbry' PROTOCOL_PREFIX = 'lbry'
APP_NAME = 'LBRY' APP_NAME = 'LBRY'

View file

@ -6,17 +6,14 @@ from binascii import hexlify
from twisted.internet import defer, reactor, threads from twisted.internet import defer, reactor, threads
from twisted.trial import unittest from twisted.trial import unittest
from orchstr8.wrapper import BaseLbryServiceStack from orchstr8.services import BaseLbryServiceStack
from lbrynet.core.call_later_manager import CallLaterManager from lbrynet.core.call_later_manager import CallLaterManager
from lbrynet.database.storage import SQLiteStorage from lbrynet.database.storage import SQLiteStorage
from lbrynet.wallet import set_wallet_manager from lbrynet.wallet.basecoin import CoinRegistry
from lbrynet.wallet.wallet import Wallet
from lbrynet.wallet.manager import WalletManager from lbrynet.wallet.manager import WalletManager
from lbrynet.wallet.transaction import Transaction, Output from lbrynet.wallet.constants import COIN
from lbrynet.wallet.constants import COIN, REGTEST_CHAIN
from lbrynet.wallet.hash import hash160_to_address, address_to_hash_160
class WalletTestCase(unittest.TestCase): class WalletTestCase(unittest.TestCase):
@ -27,11 +24,6 @@ class WalletTestCase(unittest.TestCase):
logging.getLogger('lbrynet').setLevel(logging.INFO) logging.getLogger('lbrynet').setLevel(logging.INFO)
self.data_path = tempfile.mkdtemp() self.data_path = tempfile.mkdtemp()
self.db = SQLiteStorage(self.data_path) self.db = SQLiteStorage(self.data_path)
self.config = {
'chain': REGTEST_CHAIN,
'wallet_path': self.data_path,
'default_servers': [('localhost', 50001)]
}
CallLaterManager.setup(reactor.callLater) CallLaterManager.setup(reactor.callLater)
self.service = BaseLbryServiceStack(self.VERBOSE) self.service = BaseLbryServiceStack(self.VERBOSE)
return self.service.startup() return self.service.startup()
@ -52,37 +44,30 @@ class StartupTests(WalletTestCase):
@defer.inlineCallbacks @defer.inlineCallbacks
def test_balance(self): def test_balance(self):
wallet = Wallet(chain=REGTEST_CHAIN) coin_id = 'lbc_regtest'
manager = WalletManager(self.config, wallet) manager = WalletManager.from_config({
set_wallet_manager(manager) 'ledgers': {coin_id: {'default_servers': [('localhost', 50001)]}}
yield manager.start() })
yield self.lbrycrd.generate(1) wallet = manager.create_wallet(None, CoinRegistry.get_coin_class(coin_id))
yield threads.deferToThread(time.sleep, 1) ledger = manager.ledgers.values()[0]
#yield wallet.network.on_header.first account = wallet.default_account
address = manager.get_least_used_receiving_address() coin = account.coin
yield manager.start_ledgers()
address = account.get_least_used_receiving_address()
sendtxid = yield self.lbrycrd.sendtoaddress(address, 2.5) sendtxid = yield self.lbrycrd.sendtoaddress(address, 2.5)
yield self.lbrycrd.generate(1) yield self.lbrycrd.generate(1)
#yield manager.wallet.history.on_transaction. #yield manager.wallet.history.on_transaction.
yield threads.deferToThread(time.sleep, 10) yield threads.deferToThread(time.sleep, 10)
tx = manager.ledger.transactions.values()[0] utxo = account.get_unspent_utxos()[0]
print(tx.to_python_source()) address2 = account.get_least_used_receiving_address()
print(address) tx_class = ledger.transaction_class
output = None Input, Output = tx_class.input_class, tx_class.output_class
for txo in tx.outputs: tx = tx_class()\
other = hash160_to_address(txo.script.values['pubkey_hash'], 'regtest') .add_inputs([Input.spend(utxo)])\
if other == address: .add_outputs([Output.pay_pubkey_hash(2.49*COIN, coin.address_to_hash160(address2))])\
output = txo .sign(account)
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.decoderawtransaction(hexlify(tx.raw))
yield self.lbrycrd.sendrawtransaction(hexlify(tx.raw)) yield self.lbrycrd.sendrawtransaction(hexlify(tx.raw))
yield manager.stop() yield manager.stop_ledgers()

View file

@ -0,0 +1,99 @@
from twisted.trial import unittest
from lbrynet.wallet.coins.lbc import LBC
from lbrynet.wallet.manager import WalletManager
from lbrynet.wallet.wallet import Account
class TestAccount(unittest.TestCase):
def setUp(self):
coin = LBC()
ledger = coin.ledger_class
WalletManager([], {ledger: ledger(coin)}).install()
self.coin = coin
def test_generate_account(self):
account = Account.generate(self.coin)
self.assertEqual(account.coin, self.coin)
self.assertIsNotNone(account.seed)
self.assertEqual(account.public_key.coin, self.coin)
self.assertEqual(account.private_key.public_key, account.public_key)
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)
account.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.from_seed(
self.coin,
"carbon smart garage balance margin twelve chest sword toast envelope bottom stomach ab"
"sent"
)
self.assertEqual(
account.private_key.extended_key_string(),
'LprvXPsFZUGgrX1X9HiyxABZSf6hWJK7kHv4zGZRyyiHbBq5Wu94cE1DMvttnpLYReTPNW4eYwX9dWMvTz3PrB'
'wwbRafEeA1ZXL69U2egM4QJdq'
)
self.assertEqual(
account.public_key.extended_key_string(),
'Lpub2hkYkGHXktBhLpwUhKKogyuJ1M7Gt9EkjFTVKyDqZiZpWdhLuCoT1eKDfXfysMFfG4SzfXXcA2SsHzrjHK'
'Ea5aoCNRBAhjT5NPLV6hXtvEi'
)
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(),
'LprvXTnmVLXGKvRGo2ihBE6LJ771G3VVpAx2zhTJvjnx5P3h6iZ4VJX8PvwTcgzJZ1hqXX61Wpn4pQoP6n2wgp'
'S8xjzCM6H2uGzCXuAMy5H9vtA'
)
self.assertIsNone(account.get_private_key_for_address('BcQjRlhDOIrQez1WHfz3whnB33Bp34sUgX'))
def test_load_and_save_account(self):
account_data = {
'seed':
"carbon smart garage balance margin twelve chest sword toast envelope bottom stomac"
"h absent",
'encrypted': False,
'private_key':
'LprvXPsFZUGgrX1X9HiyxABZSf6hWJK7kHv4zGZRyyiHbBq5Wu94cE1DMvttnpLYReTPNW4eYwX9dWMvTz3PrB'
'wwbRafEeA1ZXL69U2egM4QJdq',
'public_key':
'Lpub2hkYkGHXktBhLpwUhKKogyuJ1M7Gt9EkjFTVKyDqZiZpWdhLuCoT1eKDfXfysMFfG4SzfXXcA2SsHzrjHK'
'Ea5aoCNRBAhjT5NPLV6hXtvEi',
'receiving_gap': 10,
'receiving_keys': [
'02c68e2d1cf85404c86244ffa279f4c5cd00331e996d30a86d6e46480e3a9220f4',
'03c5a997d0549875d23b8e4bbc7b4d316d962587483f3a2e62ddd90a21043c4941'
],
'change_gap': 10,
'change_keys': [
'021460e8d728eee325d0d43128572b2e2bacdc027e420451df100cf9f2154ea5ab'
]
}
account = Account.from_dict(self.coin, account_data)
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'
)
account_data['coin'] = 'lbc_mainnet'
self.assertDictEqual(account_data, account.to_dict())

View file

@ -1,10 +1,12 @@
import unittest import unittest
from lbrynet.wallet.constants import CENT, MAXIMUM_FEE_PER_BYTE from lbrynet.wallet.coins.lbc.lbc import LBRYCredits
from lbrynet.wallet.transaction import Transaction, Output from lbrynet.wallet.coins.bitcoin import Bitcoin
from lbrynet.wallet.coinselection import CoinSelector, MAXIMUM_TRIES from lbrynet.wallet.coinselection import CoinSelector, MAXIMUM_TRIES
from lbrynet.wallet.constants import CENT
from lbrynet.wallet.manager import WalletManager from lbrynet.wallet.manager import WalletManager
from lbrynet.wallet import set_wallet_manager
from .test_transaction import get_output as utxo
NULL_HASH = '\x00'*32 NULL_HASH = '\x00'*32
@ -15,20 +17,18 @@ def search(*args, **kwargs):
return [o.amount for o in selection] if selection else selection return [o.amount for o in selection] if selection else selection
def utxo(amount):
return Output.pay_pubkey_hash(Transaction(), 0, amount, NULL_HASH)
class TestCoinSelectionTests(unittest.TestCase): class TestCoinSelectionTests(unittest.TestCase):
def setUp(self): def setUp(self):
set_wallet_manager(WalletManager({'fee_per_byte': MAXIMUM_FEE_PER_BYTE})) WalletManager([], {
LBRYCredits.ledger_class: LBRYCredits.ledger_class(LBRYCredits),
}).install()
def test_empty_coins(self): def test_empty_coins(self):
self.assertIsNone(CoinSelector([], 0, 0).select()) self.assertIsNone(CoinSelector([], 0, 0).select())
def test_skip_binary_search_if_total_not_enough(self): def test_skip_binary_search_if_total_not_enough(self):
fee = utxo(CENT).spend(fake=True).fee fee = utxo(CENT).spend().fee
big_pool = [utxo(CENT+fee) for _ in range(100)] big_pool = [utxo(CENT+fee) for _ in range(100)]
selector = CoinSelector(big_pool, 101 * CENT, 0) selector = CoinSelector(big_pool, 101 * CENT, 0)
self.assertIsNone(selector.select()) self.assertIsNone(selector.select())
@ -39,7 +39,7 @@ class TestCoinSelectionTests(unittest.TestCase):
self.assertEqual(selector.tries, 201) self.assertEqual(selector.tries, 201)
def test_exact_match(self): def test_exact_match(self):
fee = utxo(CENT).spend(fake=True).fee fee = utxo(CENT).spend().fee
utxo_pool = [ utxo_pool = [
utxo(CENT + fee), utxo(CENT + fee),
utxo(CENT), utxo(CENT),
@ -74,7 +74,9 @@ class TestOfficialBitcoinCoinSelectionTests(unittest.TestCase):
# https://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf # https://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf
def setUp(self): def setUp(self):
set_wallet_manager(WalletManager({'fee_per_byte': 0})) WalletManager([], {
Bitcoin.ledger_class: Bitcoin.ledger_class(Bitcoin),
}).install()
def make_hard_case(self, utxos): def make_hard_case(self, utxos):
target = 0 target = 0

View file

View file

@ -1,9 +1,11 @@
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
from twisted.trial import unittest from twisted.trial import unittest
from lbrynet.wallet.script import Template, ParseError, tokenize, push_data
from lbrynet.wallet.script import PUSH_SINGLE, PUSH_MANY, OP_HASH160, OP_EQUAL
from lbrynet.wallet.script import InputScript, OutputScript
from lbrynet.wallet.bcd_data_stream import BCDataStream from lbrynet.wallet.bcd_data_stream import BCDataStream
from lbrynet.wallet.basescript import Template, ParseError, tokenize, push_data
from lbrynet.wallet.basescript import PUSH_SINGLE, PUSH_MANY, OP_HASH160, OP_EQUAL
from lbrynet.wallet.basescript import BaseInputScript, BaseOutputScript
from lbrynet.wallet.coins.lbc.script import OutputScript
def parse(opcodes, source): def parse(opcodes, source):
@ -100,12 +102,12 @@ class TestRedeemPubKeyHash(unittest.TestCase):
def redeem_pubkey_hash(self, sig, pubkey): def redeem_pubkey_hash(self, sig, pubkey):
# this checks that factory function correctly sets up the script # this checks that factory function correctly sets up the script
src1 = InputScript.redeem_pubkey_hash(unhexlify(sig), unhexlify(pubkey)) src1 = BaseInputScript.redeem_pubkey_hash(unhexlify(sig), unhexlify(pubkey))
self.assertEqual(src1.template.name, 'pubkey_hash') self.assertEqual(src1.template.name, 'pubkey_hash')
self.assertEqual(hexlify(src1.values['signature']), sig) self.assertEqual(hexlify(src1.values['signature']), sig)
self.assertEqual(hexlify(src1.values['pubkey']), pubkey) self.assertEqual(hexlify(src1.values['pubkey']), pubkey)
# now we test that it will round trip # now we test that it will round trip
src2 = InputScript(src1.source) src2 = BaseInputScript(src1.source)
self.assertEqual(src2.template.name, 'pubkey_hash') self.assertEqual(src2.template.name, 'pubkey_hash')
self.assertEqual(hexlify(src2.values['signature']), sig) self.assertEqual(hexlify(src2.values['signature']), sig)
self.assertEqual(hexlify(src2.values['pubkey']), pubkey) self.assertEqual(hexlify(src2.values['pubkey']), pubkey)
@ -128,7 +130,7 @@ class TestRedeemScriptHash(unittest.TestCase):
def redeem_script_hash(self, sigs, pubkeys): def redeem_script_hash(self, sigs, pubkeys):
# this checks that factory function correctly sets up the script # this checks that factory function correctly sets up the script
src1 = InputScript.redeem_script_hash( src1 = BaseInputScript.redeem_script_hash(
[unhexlify(sig) for sig in sigs], [unhexlify(sig) for sig in sigs],
[unhexlify(pubkey) for pubkey in pubkeys] [unhexlify(pubkey) for pubkey in pubkeys]
) )
@ -139,7 +141,7 @@ class TestRedeemScriptHash(unittest.TestCase):
self.assertEqual(subscript1.values['signatures_count'], len(sigs)) self.assertEqual(subscript1.values['signatures_count'], len(sigs))
self.assertEqual(subscript1.values['pubkeys_count'], len(pubkeys)) self.assertEqual(subscript1.values['pubkeys_count'], len(pubkeys))
# now we test that it will round trip # now we test that it will round trip
src2 = InputScript(src1.source) src2 = BaseInputScript(src1.source)
subscript2 = src2.values['script'] subscript2 = src2.values['script']
self.assertEqual(src2.template.name, 'script_hash') self.assertEqual(src2.template.name, 'script_hash')
self.assertEqual([hexlify(v) for v in src2.values['signatures']], sigs) self.assertEqual([hexlify(v) for v in src2.values['signatures']], sigs)
@ -181,11 +183,11 @@ class TestPayPubKeyHash(unittest.TestCase):
def pay_pubkey_hash(self, pubkey_hash): def pay_pubkey_hash(self, pubkey_hash):
# this checks that factory function correctly sets up the script # this checks that factory function correctly sets up the script
src1 = OutputScript.pay_pubkey_hash(unhexlify(pubkey_hash)) src1 = BaseOutputScript.pay_pubkey_hash(unhexlify(pubkey_hash))
self.assertEqual(src1.template.name, 'pay_pubkey_hash') self.assertEqual(src1.template.name, 'pay_pubkey_hash')
self.assertEqual(hexlify(src1.values['pubkey_hash']), pubkey_hash) self.assertEqual(hexlify(src1.values['pubkey_hash']), pubkey_hash)
# now we test that it will round trip # now we test that it will round trip
src2 = OutputScript(src1.source) src2 = BaseOutputScript(src1.source)
self.assertEqual(src2.template.name, 'pay_pubkey_hash') self.assertEqual(src2.template.name, 'pay_pubkey_hash')
self.assertEqual(hexlify(src2.values['pubkey_hash']), pubkey_hash) self.assertEqual(hexlify(src2.values['pubkey_hash']), pubkey_hash)
return hexlify(src1.source) return hexlify(src1.source)
@ -201,11 +203,11 @@ class TestPayScriptHash(unittest.TestCase):
def pay_script_hash(self, script_hash): def pay_script_hash(self, script_hash):
# this checks that factory function correctly sets up the script # this checks that factory function correctly sets up the script
src1 = OutputScript.pay_script_hash(unhexlify(script_hash)) src1 = BaseOutputScript.pay_script_hash(unhexlify(script_hash))
self.assertEqual(src1.template.name, 'pay_script_hash') self.assertEqual(src1.template.name, 'pay_script_hash')
self.assertEqual(hexlify(src1.values['script_hash']), script_hash) self.assertEqual(hexlify(src1.values['script_hash']), script_hash)
# now we test that it will round trip # now we test that it will round trip
src2 = OutputScript(src1.source) src2 = BaseOutputScript(src1.source)
self.assertEqual(src2.template.name, 'pay_script_hash') self.assertEqual(src2.template.name, 'pay_script_hash')
self.assertEqual(hexlify(src2.values['script_hash']), script_hash) self.assertEqual(hexlify(src2.values['script_hash']), script_hash)
return hexlify(src1.source) return hexlify(src1.source)
@ -221,7 +223,8 @@ class TestPayClaimNamePubkeyHash(unittest.TestCase):
def pay_claim_name_pubkey_hash(self, name, claim, pubkey_hash): def pay_claim_name_pubkey_hash(self, name, claim, pubkey_hash):
# this checks that factory function correctly sets up the script # this checks that factory function correctly sets up the script
src1 = OutputScript.pay_claim_name_pubkey_hash(name, unhexlify(claim), unhexlify(pubkey_hash)) src1 = OutputScript.pay_claim_name_pubkey_hash(
name, unhexlify(claim), unhexlify(pubkey_hash))
self.assertEqual(src1.template.name, 'claim_name+pay_pubkey_hash') self.assertEqual(src1.template.name, 'claim_name+pay_pubkey_hash')
self.assertEqual(src1.values['claim_name'], name) self.assertEqual(src1.values['claim_name'], name)
self.assertEqual(hexlify(src1.values['claim']), claim) self.assertEqual(hexlify(src1.values['claim']), claim)

View file

@ -1,11 +1,12 @@
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
from twisted.trial import unittest from twisted.trial import unittest
from lbrynet.wallet.constants import CENT
from lbrynet.wallet.transaction import Transaction, Input, Output from lbrynet.wallet.account import Account
from lbrynet.wallet.coins.lbc import LBC
from lbrynet.wallet.coins.lbc.transaction import Transaction, Output, Input
from lbrynet.wallet.constants import CENT, COIN
from lbrynet.wallet.manager import WalletManager from lbrynet.wallet.manager import WalletManager
from lbrynet.wallet import set_wallet_manager from lbrynet.wallet.wallet import Wallet
from lbrynet.wallet.bip32 import PrivateKey
from lbrynet.wallet.mnemonic import Mnemonic
NULL_HASH = '\x00'*32 NULL_HASH = '\x00'*32
@ -13,68 +14,78 @@ FEE_PER_BYTE = 50
FEE_PER_CHAR = 200000 FEE_PER_CHAR = 200000
def get_output(amount=CENT, pubkey_hash=NULL_HASH):
return Transaction() \
.add_outputs([Output.pay_pubkey_hash(amount, pubkey_hash)]) \
.outputs[0]
def get_input():
return Input.spend(get_output())
def get_transaction(txo=None):
return Transaction() \
.add_inputs([get_input()]) \
.add_outputs([txo or Output.pay_pubkey_hash(CENT, NULL_HASH)])
def get_claim_transaction(claim_name, claim=''):
return get_transaction(
Output.pay_claim_name_pubkey_hash(CENT, claim_name, claim, NULL_HASH)
)
def get_lbc_wallet():
lbc = LBC.from_dict({
'fee_per_byte': FEE_PER_BYTE,
'fee_per_name_char': FEE_PER_CHAR
})
return Wallet('Main', [lbc], [Account.generate(lbc)])
class TestSizeAndFeeEstimation(unittest.TestCase): class TestSizeAndFeeEstimation(unittest.TestCase):
def setUp(self): def setUp(self):
set_wallet_manager(WalletManager({ self.wallet = get_lbc_wallet()
'fee_per_byte': FEE_PER_BYTE, self.coin = self.wallet.coins[0]
'fee_per_name_char': FEE_PER_CHAR WalletManager([self.wallet], {})
}))
@staticmethod def io_fee(self, io):
def get_output(): return self.coin.get_input_output_fee(io)
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): def test_output_size_and_fee(self):
txo = self.get_output() txo = get_output()
self.assertEqual(txo.size, 46) self.assertEqual(txo.size, 46)
self.assertEqual(txo.fee, 46 * FEE_PER_BYTE) self.assertEqual(self.io_fee(txo), 46 * FEE_PER_BYTE)
def test_input_size_and_fee(self): def test_input_size_and_fee(self):
txi = self.get_input() txi = get_input()
self.assertEqual(txi.size, 148) self.assertEqual(txi.size, 148)
self.assertEqual(txi.fee, 148 * FEE_PER_BYTE) self.assertEqual(self.io_fee(txi), 148 * FEE_PER_BYTE)
def test_transaction_size_and_fee(self): def test_transaction_size_and_fee(self):
tx = self.get_transaction() tx = get_transaction()
base_size = tx.size - 1 - tx.inputs[0].size base_size = tx.size - 1 - tx.inputs[0].size
self.assertEqual(tx.size, 204) self.assertEqual(tx.size, 204)
self.assertEqual(tx.base_size, base_size) self.assertEqual(tx.base_size, base_size)
self.assertEqual(tx.base_fee, FEE_PER_BYTE * base_size) self.assertEqual(self.coin.get_transaction_base_fee(tx), FEE_PER_BYTE * base_size)
def test_claim_name_transaction_size_and_fee(self): def test_claim_name_transaction_size_and_fee(self):
# fee based on claim name is the larger fee # fee based on claim name is the larger fee
claim_name = 'verylongname' claim_name = 'verylongname'
tx = self.get_claim_transaction(claim_name, '0'*4000) tx = get_claim_transaction(claim_name, '0'*4000)
base_size = tx.size - 1 - tx.inputs[0].size base_size = tx.size - 1 - tx.inputs[0].size
self.assertEqual(tx.size, 4225) self.assertEqual(tx.size, 4225)
self.assertEqual(tx.base_size, base_size) self.assertEqual(tx.base_size, base_size)
self.assertEqual(tx.base_fee, len(claim_name) * FEE_PER_CHAR) self.assertEqual(self.coin.get_transaction_base_fee(tx), len(claim_name) * FEE_PER_CHAR)
# fee based on total bytes is the larger fee # fee based on total bytes is the larger fee
claim_name = 'a' claim_name = 'a'
tx = self.get_claim_transaction(claim_name, '0'*4000) tx = get_claim_transaction(claim_name, '0'*4000)
base_size = tx.size - 1 - tx.inputs[0].size base_size = tx.size - 1 - tx.inputs[0].size
self.assertEqual(tx.size, 4214) self.assertEqual(tx.size, 4214)
self.assertEqual(tx.base_size, base_size) self.assertEqual(tx.base_size, base_size)
self.assertEqual(tx.base_fee, FEE_PER_BYTE * base_size) self.assertEqual(self.coin.get_transaction_base_fee(tx), FEE_PER_BYTE * base_size)
class TestTransactionSerialization(unittest.TestCase): class TestTransactionSerialization(unittest.TestCase):
@ -92,7 +103,7 @@ class TestTransactionSerialization(unittest.TestCase):
self.assertEqual(len(tx.outputs), 1) self.assertEqual(len(tx.outputs), 1)
coinbase = tx.inputs[0] coinbase = tx.inputs[0]
self.assertEqual(coinbase.output_tx_hash, NULL_HASH) self.assertEqual(coinbase.output_txid, NULL_HASH)
self.assertEqual(coinbase.output_index, 0xFFFFFFFF) self.assertEqual(coinbase.output_index, 0xFFFFFFFF)
self.assertEqual(coinbase.sequence, 0xFFFFFFFF) self.assertEqual(coinbase.sequence, 0xFFFFFFFF)
self.assertTrue(coinbase.is_coinbase) self.assertTrue(coinbase.is_coinbase)
@ -125,7 +136,7 @@ class TestTransactionSerialization(unittest.TestCase):
self.assertEqual(len(tx.outputs), 1) self.assertEqual(len(tx.outputs), 1)
coinbase = tx.inputs[0] coinbase = tx.inputs[0]
self.assertEqual(coinbase.output_tx_hash, NULL_HASH) self.assertEqual(coinbase.output_txid, NULL_HASH)
self.assertEqual(coinbase.output_index, 0xFFFFFFFF) self.assertEqual(coinbase.output_index, 0xFFFFFFFF)
self.assertEqual(coinbase.sequence, 0) self.assertEqual(coinbase.sequence, 0)
self.assertTrue(coinbase.is_coinbase) self.assertTrue(coinbase.is_coinbase)
@ -166,9 +177,9 @@ class TestTransactionSerialization(unittest.TestCase):
self.assertEqual(len(tx.inputs), 1) self.assertEqual(len(tx.inputs), 1)
self.assertEqual(len(tx.outputs), 2) self.assertEqual(len(tx.outputs), 2)
txin = tx.inputs[0] # type: Input txin = tx.inputs[0]
self.assertEqual( self.assertEqual(
hexlify(txin.output_tx_hash[::-1]), hexlify(txin.output_txid[::-1]),
b'1dfd535b6c8550ebe95abceb877f92f76f30e5ba4d3483b043386027b3e13324' b'1dfd535b6c8550ebe95abceb877f92f76f30e5ba4d3483b043386027b3e13324'
) )
self.assertEqual(txin.output_index, 0) self.assertEqual(txin.output_index, 0)
@ -186,7 +197,7 @@ class TestTransactionSerialization(unittest.TestCase):
) )
# Claim # Claim
out0 = tx.outputs[0] # type: Output out0 = tx.outputs[0]
self.assertEqual(out0.amount, 10000000) self.assertEqual(out0.amount, 10000000)
self.assertEqual(out0.index, 0) self.assertEqual(out0.index, 0)
self.assertTrue(out0.script.is_pay_pubkey_hash) self.assertTrue(out0.script.is_pay_pubkey_hash)
@ -199,7 +210,7 @@ class TestTransactionSerialization(unittest.TestCase):
) )
# Change # Change
out1 = tx.outputs[1] # type: Output out1 = tx.outputs[1]
self.assertEqual(out1.amount, 189977100) self.assertEqual(out1.amount, 189977100)
self.assertEqual(out1.index, 1) self.assertEqual(out1.index, 1)
self.assertTrue(out1.script.is_pay_pubkey_hash) self.assertTrue(out1.script.is_pay_pubkey_hash)
@ -215,15 +226,27 @@ class TestTransactionSerialization(unittest.TestCase):
class TestTransactionSigning(unittest.TestCase): 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): def test_sign(self):
tx = Transaction() lbc = LBC()
Output.pay_pubkey_hash(Transaction(), 0, CENT, NULL_HASH).spend(fake=True) wallet = Wallet('Main', [lbc], [Account.from_seed(
tx.add_inputs([self.get_input()]) lbc, 'carbon smart garage balance margin twelve chest sword toast envelope '
Output.pay_pubkey_hash(tx, 0, CENT, NULL_HASH) 'bottom stomach absent'
tx = self.get_tx() )])
account = wallet.default_account
address1 = account.receiving_keys.generate_next_address()
address2 = account.receiving_keys.generate_next_address()
pubkey_hash1 = account.coin.address_to_hash160(address1)
pubkey_hash2 = account.coin.address_to_hash160(address2)
tx = Transaction() \
.add_inputs([Input.spend(get_output(2*COIN, pubkey_hash1))]) \
.add_outputs([Output.pay_pubkey_hash(1.9*COIN, pubkey_hash2)]) \
.sign(account)
print(hexlify(tx.inputs[0].script.values['signature']))
self.assertEqual(
hexlify(tx.inputs[0].script.values['signature']),
b'304402200dafa26ad7cf38c5a971c8a25ce7d85a076235f146126762296b1223c42ae21e022020ef9eeb8'
b'398327891008c5c0be4357683f12cb22346691ff23914f457bf679601'
)

View file

@ -1,83 +1,84 @@
from twisted.trial import unittest from twisted.trial import unittest
from lbrynet.wallet.wallet import Account, Wallet
from lbrynet.wallet.coins.bitcoin import BTC
from lbrynet.wallet.coins.lbc import LBC
from lbrynet.wallet.manager import WalletManager from lbrynet.wallet.manager import WalletManager
from lbrynet.wallet import set_wallet_manager from lbrynet.wallet.wallet import Account, Wallet, WalletStorage
class TestWalletAccount(unittest.TestCase): class TestWalletCreation(unittest.TestCase):
def test_wallet_automatically_creates_default_account(self): def setUp(self):
WalletManager([], {
LBC.ledger_class: LBC.ledger_class(LBC),
BTC.ledger_class: BTC.ledger_class(BTC)
}).install()
self.coin = LBC()
def test_create_wallet_and_accounts(self):
wallet = Wallet() wallet = Wallet()
set_wallet_manager(WalletManager(wallet=wallet)) self.assertEqual(wallet.name, 'Wallet')
account = wallet.default_account # type: Account self.assertEqual(wallet.coins, [])
self.assertIsInstance(account, Account) self.assertEqual(wallet.accounts, [])
self.assertEqual(len(account.receiving_keys.child_keys), 0)
self.assertEqual(len(account.receiving_keys.addresses), 0) account1 = wallet.generate_account(LBC)
self.assertEqual(len(account.change_keys.child_keys), 0) account2 = wallet.generate_account(LBC)
self.assertEqual(len(account.change_keys.addresses), 0) account3 = wallet.generate_account(BTC)
self.assertEqual(wallet.default_account, account1)
self.assertEqual(len(wallet.coins), 2)
self.assertEqual(len(wallet.accounts), 3)
self.assertIsInstance(wallet.coins[0], LBC)
self.assertIsInstance(wallet.coins[1], BTC)
self.assertEqual(len(account1.receiving_keys.addresses), 0)
self.assertEqual(len(account1.change_keys.addresses), 0)
self.assertEqual(len(account2.receiving_keys.addresses), 0)
self.assertEqual(len(account2.change_keys.addresses), 0)
self.assertEqual(len(account3.receiving_keys.addresses), 0)
self.assertEqual(len(account3.change_keys.addresses), 0)
wallet.ensure_enough_addresses() wallet.ensure_enough_addresses()
self.assertEqual(len(account.receiving_keys.child_keys), 20) self.assertEqual(len(account1.receiving_keys.addresses), 20)
self.assertEqual(len(account.receiving_keys.addresses), 20) self.assertEqual(len(account1.change_keys.addresses), 6)
self.assertEqual(len(account.change_keys.child_keys), 6) self.assertEqual(len(account2.receiving_keys.addresses), 20)
self.assertEqual(len(account.change_keys.addresses), 6) self.assertEqual(len(account2.change_keys.addresses), 6)
self.assertEqual(len(account3.receiving_keys.addresses), 20)
self.assertEqual(len(account3.change_keys.addresses), 6)
def test_generate_account_from_seed(self): def test_load_and_save_wallet(self):
account = Account.generate_from_seed( wallet_dict = {
"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', 'name': 'Main Wallet',
'accounts': { 'accounts': [
0: { {
'coin': 'lbc_mainnet',
'seed': 'seed':
"carbon smart garage balance margin twelve chest sword toast envelope botto" "carbon smart garage balance margin twelve chest sword toast envelope botto"
"m stomach absent", "m stomach absent",
'encrypted': False, 'encrypted': False,
'private_key': 'private_key':
"xprv9s21ZrQH143K42ovpZygnjfHdAqSd9jo7zceDfPRogM7bkkoNVv7DRNLEoB8HoirMgH969" 'LprvXPsFZUGgrX1X9HiyxABZSf6hWJK7kHv4zGZRyyiHbBq5Wu94cE1DMvttnpLYReTPNW4eYwX9dWMvTz3PrB'
"NrgL8jNzLEegqFzPRWM37GXd4uE8uuRkx4LAe", 'wwbRafEeA1ZXL69U2egM4QJdq',
'public_key': 'public_key':
"xpub661MyMwAqRbcGWtPvbWh9sc2BCfw2cTeVDYF23o3N1t6UZ5wv3EMmDgp66FxHuDtWdft3B" 'Lpub2hkYkGHXktBhLpwUhKKogyuJ1M7Gt9EkjFTVKyDqZiZpWdhLuCoT1eKDfXfysMFfG4SzfXXcA2SsHzrjHK'
"5eL5xQtyzAtkdmhhC95gjRjLzSTdkho95asu9", 'Ea5aoCNRBAhjT5NPLV6hXtvEi',
'receiving_gap': 10, 'receiving_gap': 10,
'receiving_keys': [ 'receiving_keys': [
'02c68e2d1cf85404c86244ffa279f4c5cd00331e996d30a86d6e46480e3a9220f4', '02c68e2d1cf85404c86244ffa279f4c5cd00331e996d30a86d6e46480e3a9220f4',
'03c5a997d0549875d23b8e4bbc7b4d316d962587483f3a2e62ddd90a21043c4941'], '03c5a997d0549875d23b8e4bbc7b4d316d962587483f3a2e62ddd90a21043c4941'
],
'change_gap': 10, 'change_gap': 10,
'change_keys': [ 'change_keys': [
'021460e8d728eee325d0d43128572b2e2bacdc027e420451df100cf9f2154ea5ab'] '021460e8d728eee325d0d43128572b2e2bacdc027e420451df100cf9f2154ea5ab'
]
} }
} ]
} }
wallet = Wallet.from_json(wallet_data) storage = WalletStorage(default=wallet_dict)
set_wallet_manager(WalletManager(wallet=wallet)) wallet = Wallet.from_storage(storage)
self.assertEqual(wallet.name, 'Main Wallet') self.assertEqual(wallet.name, 'Main Wallet')
self.assertEqual(len(wallet.coins), 1)
self.assertIsInstance(wallet.coins[0], LBC)
self.assertEqual(len(wallet.accounts), 1)
account = wallet.default_account account = wallet.default_account
self.assertIsInstance(account, Account) self.assertIsInstance(account, Account)
@ -91,8 +92,5 @@ class TestWalletAccount(unittest.TestCase):
account.change_keys.addresses[0], account.change_keys.addresses[0],
'bFpHENtqugKKHDshKFq2Mnb59Y2bx4vKgL' 'bFpHENtqugKKHDshKFq2Mnb59Y2bx4vKgL'
) )
wallet_dict['coins'] = {'lbc_mainnet': {'fee_per_name_char': 200000, 'fee_per_byte': 50}}
self.assertDictEqual( self.assertDictEqual(wallet_dict, wallet.to_dict())
wallet_data['accounts'][0],
account.to_json()
)

View file

@ -1,10 +1 @@
_wallet_manager = None import coins
def set_wallet_manager(wallet_manager):
global _wallet_manager
_wallet_manager = wallet_manager
def get_wallet_manager():
return _wallet_manager

View file

@ -1,21 +1,22 @@
import itertools
from typing import Dict, Generator
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
from itertools import chain
from lbrynet.wallet import get_wallet_manager from lbrynet.wallet.basecoin import BaseCoin
from lbrynet.wallet.mnemonic import Mnemonic from lbrynet.wallet.mnemonic import Mnemonic
from lbrynet.wallet.bip32 import PrivateKey, PubKey, from_extended_key_string from lbrynet.wallet.bip32 import PrivateKey, PubKey, from_extended_key_string
from lbrynet.wallet.hash import double_sha256, aes_encrypt, aes_decrypt from lbrynet.wallet.hash import double_sha256, aes_encrypt, aes_decrypt
from lbryschema.address import public_key_to_address
class KeyChain: class KeyChain:
def __init__(self, parent_key, child_keys, gap): def __init__(self, parent_key, child_keys, gap):
self.coin = parent_key.coin
self.parent_key = parent_key # type: PubKey self.parent_key = parent_key # type: PubKey
self.child_keys = child_keys self.child_keys = child_keys
self.minimum_gap = gap self.minimum_gap = gap
self.addresses = [ self.addresses = [
public_key_to_address(key) self.coin.public_key_to_address(key)
for key in child_keys for key in child_keys
] ]
@ -23,9 +24,8 @@ class KeyChain:
def has_gap(self): def has_gap(self):
if len(self.addresses) < self.minimum_gap: if len(self.addresses) < self.minimum_gap:
return False return False
ledger = get_wallet_manager().ledger
for address in self.addresses[-self.minimum_gap:]: for address in self.addresses[-self.minimum_gap:]:
if ledger.is_address_old(address): if self.coin.ledger.is_address_old(address):
return False return False
return True return True
@ -44,71 +44,77 @@ class KeyChain:
class Account: class Account:
def __init__(self, seed, encrypted, private_key, public_key, **kwargs): def __init__(self, coin, seed, encrypted, private_key, public_key,
self.seed = seed receiving_keys=None, receiving_gap=20,
self.encrypted = encrypted change_keys=None, change_gap=6):
self.coin = coin # type: BaseCoin
self.seed = seed # type: str
self.encrypted = encrypted # type: bool
self.private_key = private_key # type: PrivateKey self.private_key = private_key # type: PrivateKey
self.public_key = public_key # type: PubKey self.public_key = public_key # type: PubKey
self.receiving_gap = kwargs.get('receiving_gap', 20) self.keychains = (
self.receiving_keys = kwargs.get('receiving_keys') or \ KeyChain(public_key.child(0), receiving_keys or [], receiving_gap),
KeyChain(self.public_key.child(0), [], self.receiving_gap) KeyChain(public_key.child(1), change_keys or [], change_gap)
self.change_gap = kwargs.get('change_gap', 6) )
self.change_keys = kwargs.get('change_keys') or \ self.receiving_keys, self.change_keys = self.keychains
KeyChain(self.public_key.child(1), [], self.change_gap)
self.keychains = [
self.receiving_keys, # child: 0
self.change_keys # child: 1
]
@classmethod @classmethod
def generate(cls): def generate(cls, coin): # type: (BaseCoin) -> Account
seed = Mnemonic().make_seed() seed = Mnemonic().make_seed()
return cls.generate_from_seed(seed) return cls.from_seed(coin, seed)
@classmethod @classmethod
def generate_from_seed(cls, seed): def from_seed(cls, coin, seed): # type: (BaseCoin, str) -> Account
private_key = cls.get_private_key_from_seed(seed) private_key = cls.get_private_key_from_seed(coin, seed)
return cls( return cls(
seed=seed, encrypted=False, coin=coin, seed=seed, encrypted=False,
private_key=private_key, private_key=private_key,
public_key=private_key.public_key, public_key=private_key.public_key
) )
@staticmethod
def get_private_key_from_seed(coin, seed): # type: (BaseCoin, str) -> PrivateKey
return PrivateKey.from_seed(coin, Mnemonic.mnemonic_to_seed(seed))
@classmethod @classmethod
def from_json(cls, json_data): def from_dict(cls, coin, d): # type: (BaseCoin, Dict) -> Account
data = json_data.copy() if not d['encrypted']:
if not data['encrypted']: private_key = from_extended_key_string(coin, d['private_key'])
data['private_key'] = from_extended_key_string(data['private_key']) public_key = private_key.public_key
data['public_key'] = from_extended_key_string(data['public_key']) else:
data['receiving_keys'] = KeyChain( private_key = d['private_key']
data['public_key'].child(0), public_key = from_extended_key_string(coin, d['public_key'])
[unhexlify(k) for k in data['receiving_keys']], return cls(
data['receiving_gap'] coin=coin,
seed=d['seed'],
encrypted=d['encrypted'],
private_key=private_key,
public_key=public_key,
receiving_keys=map(unhexlify, d['receiving_keys']),
receiving_gap=d['receiving_gap'],
change_keys=map(unhexlify, d['change_keys']),
change_gap=d['change_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): def to_dict(self):
return { return {
'coin': self.coin.get_id(),
'seed': self.seed, 'seed': self.seed,
'encrypted': self.encrypted, 'encrypted': self.encrypted,
'private_key': self.private_key.extended_key_string(), 'private_key': self.private_key if self.encrypted else
self.private_key.extended_key_string(),
'public_key': self.public_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_keys': [hexlify(k) for k in self.receiving_keys.child_keys],
'receiving_gap': self.receiving_gap, 'receiving_gap': self.receiving_keys.minimum_gap,
'change_keys': [hexlify(k) for k in self.change_keys.child_keys], 'change_keys': [hexlify(k) for k in self.change_keys.child_keys],
'change_gap': self.change_gap 'change_gap': self.change_keys.minimum_gap
} }
def decrypt(self, password): def decrypt(self, password):
assert self.encrypted, "Key is not encrypted." assert self.encrypted, "Key is not encrypted."
secret = double_sha256(password) secret = double_sha256(password)
self.seed = aes_decrypt(secret, self.seed) self.seed = aes_decrypt(secret, self.seed)
self.private_key = from_extended_key_string(aes_decrypt(secret, self.private_key)) self.private_key = from_extended_key_string(self.coin, aes_decrypt(secret, self.private_key))
self.encrypted = False self.encrypted = False
def encrypt(self, password): def encrypt(self, password):
@ -118,13 +124,9 @@ class Account:
self.private_key = aes_encrypt(secret, self.private_key.extended_key_string()) self.private_key = aes_encrypt(secret, self.private_key.extended_key_string())
self.encrypted = True self.encrypted = True
@staticmethod
def get_private_key_from_seed(seed):
return PrivateKey.from_seed(Mnemonic.mnemonic_to_seed(seed))
@property @property
def addresses(self): def addresses(self):
return chain(self.receiving_keys.addresses, self.change_keys.addresses) return itertools.chain(self.receiving_keys.addresses, self.change_keys.addresses)
def get_private_key_for_address(self, address): def get_private_key_for_address(self, address):
assert not self.encrypted, "Cannot get private key on encrypted wallet account." assert not self.encrypted, "Cannot get private key on encrypted wallet account."
@ -139,3 +141,47 @@ class Account:
for keychain in self.keychains for keychain in self.keychains
for address in keychain.ensure_enough_addresses() for address in keychain.ensure_enough_addresses()
] ]
def addresses_without_history(self):
for address in self.addresses:
if not self.coin.ledger.has_address(address):
yield address
def get_least_used_receiving_address(self, max_transactions=1000):
return self._get_least_used_address(
self.receiving_keys.addresses,
self.receiving_keys,
max_transactions
)
def get_least_used_change_address(self, max_transactions=100):
return self._get_least_used_address(
self.change_keys.addresses,
self.change_keys,
max_transactions
)
def _get_least_used_address(self, addresses, keychain, max_transactions):
ledger = self.coin.ledger
address = ledger.get_least_used_address(addresses, max_transactions)
if address:
return address
address = keychain.generate_next_address()
ledger.subscribe_history(address)
return address
def get_unspent_utxos(self):
return [
utxo
for address in self.addresses
for utxo in self.coin.ledger.get_unspent_outputs(address)
]
class AccountsView:
def __init__(self, accounts):
self._accounts_generator = accounts
def __iter__(self): # type: () -> Generator[Account]
return self._accounts_generator()

View file

@ -0,0 +1,83 @@
import six
from typing import Dict, Type
from .hash import hash160, double_sha256, Base58
class CoinRegistry(type):
coins = {} # type: Dict[str, Type[BaseCoin]]
def __new__(mcs, name, bases, attrs):
cls = super(CoinRegistry, mcs).__new__(mcs, name, bases, attrs) # type: Type[BaseCoin]
if not (name == 'BaseCoin' and not bases):
coin_id = cls.get_id()
assert coin_id not in mcs.coins, 'Coin with id "{}" already registered.'.format(coin_id)
mcs.coins[coin_id] = cls
assert cls.ledger_class.coin_class is None, (
"Ledger ({}) which this coin ({}) references is already referenced by another "
"coin ({}). One to one relationship between a coin and a ledger is strictly and "
"automatically enforced. Make sure that coin_class=None in the ledger and that "
"another Coin isn't already referencing this Ledger."
).format(cls.ledger_class.__name__, name, cls.ledger_class.coin_class.__name__)
# create back reference from ledger to the coin
cls.ledger_class.coin_class = cls
return cls
@classmethod
def get_coin_class(mcs, coin_id): # type: (str) -> Type[BaseCoin]
return mcs.coins[coin_id]
@classmethod
def get_ledger_class(mcs, coin_id): # type: (str) -> Type[BaseLedger]
return mcs.coins[coin_id].ledger_class
class BaseCoin(six.with_metaclass(CoinRegistry)):
name = None
symbol = None
network = None
ledger_class = None # type: Type[BaseLedger]
transaction_class = None # type: Type[BaseTransaction]
secret_prefix = None
pubkey_address_prefix = None
script_address_prefix = None
extended_public_key_prefix = None
extended_private_key_prefix = None
def __init__(self, ledger, fee_per_byte):
self.ledger = ledger
self.fee_per_byte = fee_per_byte
@classmethod
def get_id(cls):
return '{}_{}'.format(cls.symbol.lower(), cls.network.lower())
def to_dict(self):
return {'fee_per_byte': self.fee_per_byte}
def get_input_output_fee(self, io):
""" Fee based on size of the input / output. """
return self.fee_per_byte * io.size
def get_transaction_base_fee(self, tx):
""" Fee for the transaction header and all outputs; without inputs. """
return self.fee_per_byte * tx.base_size
def hash160_to_address(self, h160):
raw_address = self.pubkey_address_prefix + h160
return Base58.encode(raw_address + double_sha256(raw_address)[0:4])
@staticmethod
def address_to_hash160(address):
bytes = Base58.decode(address)
prefix, pubkey_bytes, addr_checksum = bytes[0], bytes[1:21], bytes[21:]
return pubkey_bytes
def public_key_to_address(self, public_key):
return self.hash160_to_address(hash160(public_key))
@staticmethod
def private_key_to_wif(private_key):
return b'\x1c' + private_key + b'\x01'

View file

@ -2,13 +2,17 @@ import os
import logging import logging
import hashlib import hashlib
from binascii import hexlify from binascii import hexlify
from typing import List, Dict, Type
from binascii import unhexlify
from operator import itemgetter from operator import itemgetter
from twisted.internet import threads, defer from twisted.internet import threads, defer
from lbrynet.wallet.account import Account, AccountsView
from lbrynet.wallet.basecoin import BaseCoin
from lbrynet.wallet.basetransaction import BaseTransaction, BaseInput, BaseOutput
from lbrynet.wallet.basenetwork import BaseNetwork
from lbrynet.wallet.stream import StreamController, execute_serially 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.util import hex_to_int, int_to_hex, rev_hex, hash_encode
from lbrynet.wallet.hash import double_sha256, pow_hash from lbrynet.wallet.hash import double_sha256, pow_hash
@ -17,43 +21,76 @@ log = logging.getLogger(__name__)
class Address: class Address:
def __init__(self, address): def __init__(self, pubkey_hash):
self.address = address self.pubkey_hash = pubkey_hash
self.transactions = [] self.transactions = [] # type: List[BaseTransaction]
def __iter__(self):
return iter(self.transactions)
def __len__(self):
return len(self.transactions)
def add_transaction(self, transaction): def add_transaction(self, transaction):
self.transactions.append(transaction) self.transactions.append(transaction)
def get_unspent_utxos(self):
inputs, outputs, utxos = [], [], []
for tx in self:
for txi in tx.inputs:
inputs.append((txi.output_txid, txi.output_index))
for txo in tx.outputs:
if txo.script.is_pay_pubkey_hash and txo.script.values['pubkey_hash'] == self.pubkey_hash:
outputs.append((txo, txo.transaction.hash, txo.index))
for output in set(outputs):
if output[1:] not in inputs:
yield output[0]
class Ledger:
def __init__(self, config=None, db=None): class BaseLedger:
# coin_class is automatically set by BaseCoin metaclass
# when it creates the Coin classes, there is a 1..1 relationship
# between a coin and a ledger (at the class level) but a 1..* relationship
# at instance level. Only one Ledger instance should exist per coin class,
# but many coin instances can exist linking back to the single Ledger instance.
coin_class = None # type: Type[BaseCoin]
network_class = None # type: Type[BaseNetwork]
verify_bits_to_target = True
def __init__(self, accounts, config=None, network=None, db=None):
self.accounts = accounts # type: AccountsView
self.config = config or {} self.config = config or {}
self.db = db self.db = db
self.addresses = {} self.addresses = {} # type: Dict[str, Address]
self.transactions = {} self.transactions = {} # type: Dict[str, BaseTransaction]
self.headers = BlockchainHeaders(self.headers_path, self.config.get('chain', MAIN_CHAIN)) self.headers = Headers(self)
self._on_transaction_controller = StreamController() self._on_transaction_controller = StreamController()
self.on_transaction = self._on_transaction_controller.stream self.on_transaction = self._on_transaction_controller.stream
self.network = network or self.network_class(self.config)
self.network.on_header.listen(self.process_header)
self.network.on_status.listen(self.process_status)
@property @property
def headers_path(self): def transaction_class(self):
filename = 'blockchain_headers' return self.coin_class.transaction_class
if self.config.get('chain', MAIN_CHAIN) != MAIN_CHAIN:
filename = '{}_headers'.format(self.config['chain']) @classmethod
return os.path.join(self.config.get('wallet_path', ''), filename) def from_json(cls, json_dict):
return cls(json_dict)
@defer.inlineCallbacks @defer.inlineCallbacks
def load(self): def load(self):
txs = yield self.db.get_transactions() txs = yield self.db.get_transactions()
for tx_hash, raw, height in txs: for tx_hash, raw, height in txs:
self.transactions[tx_hash] = Transaction(raw, height) self.transactions[tx_hash] = self.transaction_class(raw, height)
txios = yield self.db.get_transaction_inputs_and_outputs() txios = yield self.db.get_transaction_inputs_and_outputs()
for tx_hash, address_hash, input_output, amount, height in txios: for tx_hash, address_hash, input_output, amount, height in txios:
tx = self.transactions[tx_hash] tx = self.transactions[tx_hash]
address = self.addresses.get(address_hash) address = self.addresses.get(address_hash)
if address is None: if address is None:
address = self.addresses[address_hash] = Address(address_hash) address = self.addresses[address_hash] = Address(self.coin_class.address_to_hash160(address_hash))
tx.add_txio(address, input_output, amount) tx.add_txio(address, input_output, amount)
address.add_transaction(tx) address.add_transaction(tx)
@ -68,10 +105,11 @@ class Ledger:
age = tx_age age = tx_age
return age > age_limit return age > age_limit
def add_transaction(self, address, transaction): def add_transaction(self, address, transaction): # type: (str, BaseTransaction) -> None
if address not in self.addresses:
self.addresses[address] = Address(self.coin_class.address_to_hash160(address))
self.addresses[address].add_transaction(transaction)
self.transactions.setdefault(hexlify(transaction.id), transaction) self.transactions.setdefault(hexlify(transaction.id), transaction)
self.addresses.setdefault(address, [])
self.addresses[address].append(transaction)
self._on_transaction_controller.add(transaction) self._on_transaction_controller.add(transaction)
def has_address(self, address): def has_address(self, address):
@ -109,20 +147,109 @@ class Ledger:
transaction_counts.sort(key=itemgetter(1)) transaction_counts.sort(key=itemgetter(1))
return transaction_counts[0] return transaction_counts[0]
def get_unspent_outputs(self, address):
if address in self.addresses:
return list(self.addresses[address].get_unspent_utxos())
return []
class BlockchainHeaders: @defer.inlineCallbacks
def start(self):
first_connection = self.network.on_connected.first
self.network.start()
yield first_connection
self.headers.touch()
yield self.update_headers()
yield self.network.subscribe_headers()
yield self.update_accounts()
def __init__(self, path, chain=MAIN_CHAIN): def stop(self):
self.path = path return self.network.stop()
self.chain = chain
self.max_target = CHAINS[chain]['max_target']
self.target_timespan = CHAINS[chain]['target_timespan']
self.genesis_bits = CHAINS[chain]['genesis_bits']
@execute_serially
@defer.inlineCallbacks
def update_headers(self):
while True:
height_sought = len(self.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'))
@defer.inlineCallbacks
def process_header(self, response):
header = response[0]
if self.update_headers.is_running:
return
if header['height'] == len(self.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):
# New header is several heights ahead of local, do download instead.
yield self.update_headers()
@execute_serially
def update_accounts(self):
return defer.DeferredList([
self.update_account(a) for a in self.accounts
])
@defer.inlineCallbacks
def update_account(self, account): # type: (Account) -> defer.Defferred
# 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.
account.ensure_enough_addresses()
addresses = list(account.addresses_without_history())
while addresses:
yield defer.DeferredList([
self.update_history(a) for a in addresses
])
addresses = account.ensure_enough_addresses()
# By this point all of the addresses should be restored and we
# can now subscribe all of them to receive updates.
yield defer.DeferredList([
self.subscribe_history(address)
for address in account.addresses
])
@defer.inlineCallbacks
def update_history(self, address):
history = yield self.network.get_history(address)
for hash in map(itemgetter('tx_hash'), history):
transaction = self.get_transaction(hash)
if not transaction:
raw = yield self.network.get_transaction(hash)
transaction = self.transaction_class(unhexlify(raw))
self.add_transaction(address, transaction)
@defer.inlineCallbacks
def subscribe_history(self, address):
status = yield self.network.subscribe_address(address)
if status != self.get_status(address):
self.update_history(address)
def process_status(self, response):
address, status = response
if status != self.get_status(address):
self.update_history(address)
class Headers:
def __init__(self, ledger):
self.ledger = ledger
self._size = None
self._on_change_controller = StreamController() self._on_change_controller = StreamController()
self.on_changed = self._on_change_controller.stream self.on_changed = self._on_change_controller.stream
self._size = None @property
def path(self):
wallet_path = self.ledger.config.get('wallet_path', '')
filename = '{}_headers'.format(self.ledger.coin_class.get_id())
return os.path.join(wallet_path, filename)
def touch(self): def touch(self):
if not os.path.exists(self.path): if not os.path.exists(self.path):
@ -134,13 +261,13 @@ class BlockchainHeaders:
return len(self) - 1 return len(self) - 1
def sync_read_length(self): def sync_read_length(self):
return os.path.getsize(self.path) / HEADER_SIZE return os.path.getsize(self.path) / self.ledger.header_size
def sync_read_header(self, height): def sync_read_header(self, height):
if 0 <= height < len(self): if 0 <= height < len(self):
with open(self.path, 'rb') as f: with open(self.path, 'rb') as f:
f.seek(height * HEADER_SIZE) f.seek(height * self.ledger.header_size)
return f.read(HEADER_SIZE) return f.read(self.ledger.header_size)
def __len__(self): def __len__(self):
if self._size is None: if self._size is None:
@ -168,7 +295,7 @@ class BlockchainHeaders:
previous_header = header previous_header = header
with open(self.path, 'r+b') as f: with open(self.path, 'r+b') as f:
f.seek(start * HEADER_SIZE) f.seek(start * self.ledger.header_size)
f.write(headers) f.write(headers)
f.truncate() f.truncate()
@ -179,9 +306,9 @@ class BlockchainHeaders:
self._on_change_controller.add(change) self._on_change_controller.add(change)
def _iterate_headers(self, height, headers): def _iterate_headers(self, height, headers):
assert len(headers) % HEADER_SIZE == 0 assert len(headers) % self.ledger.header_size == 0
for idx in range(len(headers) / HEADER_SIZE): for idx in range(len(headers) / self.ledger.header_size):
start, end = idx * HEADER_SIZE, (idx + 1) * HEADER_SIZE start, end = idx * self.ledger.header_size, (idx + 1) * self.ledger.header_size
header = headers[start:end] header = headers[start:end]
yield self._deserialize(height+idx, header) yield self._deserialize(height+idx, header)
@ -239,10 +366,9 @@ class BlockchainHeaders:
""" See: lbrycrd/src/lbry.cpp """ """ See: lbrycrd/src/lbry.cpp """
if height == 0: if height == 0:
return self.genesis_bits, self.max_target return self.ledger.genesis_bits, self.ledger.max_target
# bits to target if self.ledger.verify_bits_to_target:
if self.chain != REGTEST_CHAIN:
bits = last['bits'] bits = last['bits']
bitsN = (bits >> 24) & 0xff bitsN = (bits >> 24) & 0xff
assert 0x03 <= bitsN <= 0x1f, \ assert 0x03 <= bitsN <= 0x1f, \
@ -252,7 +378,7 @@ class BlockchainHeaders:
"Second part of bits should be in [0x8000, 0x7fffff] but it was {}".format(bitsBase) "Second part of bits should be in [0x8000, 0x7fffff] but it was {}".format(bitsBase)
# new target # new target
retargetTimespan = self.target_timespan retargetTimespan = self.ledger.target_timespan
nActualTimespan = last['timestamp'] - first['timestamp'] nActualTimespan = last['timestamp'] - first['timestamp']
nModulatedTimespan = retargetTimespan + (nActualTimespan - retargetTimespan) // 8 nModulatedTimespan = retargetTimespan + (nActualTimespan - retargetTimespan) // 8
@ -267,7 +393,7 @@ class BlockchainHeaders:
nModulatedTimespan = nMaxTimespan nModulatedTimespan = nMaxTimespan
# Retarget # Retarget
bnPowLimit = _ArithUint256(self.max_target) bnPowLimit = _ArithUint256(self.ledger.max_target)
bnNew = _ArithUint256.SetCompact(last['bits']) bnNew = _ArithUint256.SetCompact(last['bits'])
bnNew *= nModulatedTimespan bnNew *= nModulatedTimespan
bnNew //= nModulatedTimespan bnNew //= nModulatedTimespan

View file

@ -10,7 +10,7 @@ from twisted.protocols.basic import LineOnlyReceiver
from errors import RemoteServiceException, ProtocolException from errors import RemoteServiceException, ProtocolException
from errors import TransportException from errors import TransportException
from .stream import StreamController from lbrynet.wallet.stream import StreamController
log = logging.getLogger() log = logging.getLogger()
@ -18,6 +18,8 @@ log = logging.getLogger()
def unicode2bytes(string): def unicode2bytes(string):
if isinstance(string, six.text_type): if isinstance(string, six.text_type):
return string.encode('iso-8859-1') return string.encode('iso-8859-1')
elif isinstance(string, list):
return [unicode2bytes(s) for s in string]
return string return string
@ -125,7 +127,7 @@ class StratumClientFactory(protocol.ClientFactory):
return client return client
class Network: class BaseNetwork:
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config

View file

@ -2,8 +2,8 @@ from itertools import chain
from binascii import hexlify from binascii import hexlify
from collections import namedtuple from collections import namedtuple
from .bcd_data_stream import BCDataStream from lbrynet.wallet.bcd_data_stream import BCDataStream
from .util import subclass_tuple from lbrynet.wallet.util import subclass_tuple
# bitcoin opcodes # bitcoin opcodes
OP_0 = 0x00 OP_0 = 0x00
@ -21,11 +21,6 @@ OP_PUSHDATA4 = 0x4e
OP_2DROP = 0x6d OP_2DROP = 0x6d
OP_DROP = 0x75 OP_DROP = 0x75
# lbry custom opcodes
OP_CLAIM_NAME = 0xb5
OP_SUPPORT_CLAIM = 0xb6
OP_UPDATE_CLAIM = 0xb7
# template matching opcodes (not real opcodes) # template matching opcodes (not real opcodes)
# base class for PUSH_DATA related opcodes # base class for PUSH_DATA related opcodes
@ -289,12 +284,7 @@ class Script(object):
@classmethod @classmethod
def from_source_with_template(cls, source, template): def from_source_with_template(cls, source, template):
if template in InputScript.templates: return cls(source, template_hint=template)
return InputScript(source, template_hint=template)
elif template in OutputScript.templates:
return OutputScript(source, template_hint=template)
else:
return cls(source, template_hint=template)
def parse(self, template_hint=None): def parse(self, template_hint=None):
tokens = self.tokens tokens = self.tokens
@ -313,7 +303,7 @@ class Script(object):
self.source = self.template.generate(self.values) self.source = self.template.generate(self.values)
class InputScript(Script): class BaseInputScript(Script):
""" Input / redeem script templates (aka scriptSig) """ """ Input / redeem script templates (aka scriptSig) """
__slots__ = () __slots__ = ()
@ -362,7 +352,7 @@ class InputScript(Script):
}) })
class OutputScript(Script): class BaseOutputScript(Script):
__slots__ = () __slots__ = ()
@ -374,48 +364,9 @@ class OutputScript(Script):
OP_HASH160, PUSH_SINGLE('script_hash'), OP_EQUAL OP_HASH160, PUSH_SINGLE('script_hash'), OP_EQUAL
)) ))
CLAIM_NAME_OPCODES = (
OP_CLAIM_NAME, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim'),
OP_2DROP, OP_DROP
)
CLAIM_NAME_PUBKEY = Template('claim_name+pay_pubkey_hash', (
CLAIM_NAME_OPCODES + PAY_PUBKEY_HASH.opcodes
))
CLAIM_NAME_SCRIPT = Template('claim_name+pay_script_hash', (
CLAIM_NAME_OPCODES + PAY_SCRIPT_HASH.opcodes
))
SUPPORT_CLAIM_OPCODES = (
OP_SUPPORT_CLAIM, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim_id'),
OP_2DROP, OP_DROP
)
SUPPORT_CLAIM_PUBKEY = Template('support_claim+pay_pubkey_hash', (
SUPPORT_CLAIM_OPCODES + PAY_PUBKEY_HASH.opcodes
))
SUPPORT_CLAIM_SCRIPT = Template('support_claim+pay_script_hash', (
SUPPORT_CLAIM_OPCODES + PAY_SCRIPT_HASH.opcodes
))
UPDATE_CLAIM_OPCODES = (
OP_UPDATE_CLAIM, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim_id'), PUSH_SINGLE('claim'),
OP_2DROP, OP_2DROP
)
UPDATE_CLAIM_PUBKEY = Template('update_claim+pay_pubkey_hash', (
UPDATE_CLAIM_OPCODES + PAY_PUBKEY_HASH.opcodes
))
UPDATE_CLAIM_SCRIPT = Template('update_claim+pay_script_hash', (
UPDATE_CLAIM_OPCODES + PAY_SCRIPT_HASH.opcodes
))
templates = [ templates = [
PAY_PUBKEY_HASH, PAY_PUBKEY_HASH,
PAY_SCRIPT_HASH, PAY_SCRIPT_HASH,
CLAIM_NAME_PUBKEY,
CLAIM_NAME_SCRIPT,
SUPPORT_CLAIM_PUBKEY,
SUPPORT_CLAIM_SCRIPT,
UPDATE_CLAIM_PUBKEY,
UPDATE_CLAIM_SCRIPT
] ]
@classmethod @classmethod
@ -430,14 +381,6 @@ class OutputScript(Script):
'script_hash': script_hash 'script_hash': script_hash
}) })
@classmethod
def pay_claim_name_pubkey_hash(cls, claim_name, claim, pubkey_hash):
return cls(template=cls.CLAIM_NAME_PUBKEY, values={
'claim_name': claim_name,
'claim': claim,
'pubkey_hash': pubkey_hash
})
@property @property
def is_pay_pubkey_hash(self): def is_pay_pubkey_hash(self):
return self.template.name.endswith('pay_pubkey_hash') return self.template.name.endswith('pay_pubkey_hash')
@ -445,19 +388,3 @@ class OutputScript(Script):
@property @property
def is_pay_script_hash(self): def is_pay_script_hash(self):
return self.template.name.endswith('pay_script_hash') return self.template.name.endswith('pay_script_hash')
@property
def is_claim_name(self):
return self.template.name.startswith('claim_name+')
@property
def is_support_claim(self):
return self.template.name.startswith('support_claim+')
@property
def is_update_claim(self):
return self.template.name.startswith('update_claim+')
@property
def is_claim_involved(self):
return self.is_claim_name or self.is_support_claim or self.is_update_claim

View file

@ -1,14 +1,12 @@
import io
import six import six
import logging import logging
from binascii import hexlify
from typing import List from typing import List
from lbrynet.wallet import get_wallet_manager from lbrynet.wallet.basescript import BaseInputScript, BaseOutputScript
from lbrynet.wallet.bcd_data_stream import BCDataStream from lbrynet.wallet.bcd_data_stream import BCDataStream
from lbrynet.wallet.hash import sha256, hash160_to_address, claim_id_hash from lbrynet.wallet.hash import sha256
from lbrynet.wallet.script import InputScript, OutputScript from lbrynet.wallet.account import Account
from lbrynet.wallet.wallet import Wallet from lbrynet.wallet.util import ReadOnlyList
log = logging.getLogger() log = logging.getLogger()
@ -19,11 +17,6 @@ NULL_HASH = '\x00'*32
class InputOutput(object): class InputOutput(object):
@property
def fee(self):
""" Fee based on size of the input / output. """
return get_wallet_manager().fee_per_byte * self.size
@property @property
def size(self): def size(self):
""" Size of this input / output in bytes. """ """ Size of this input / output in bytes. """
@ -35,30 +28,39 @@ class InputOutput(object):
raise NotImplemented raise NotImplemented
class Input(InputOutput): class BaseInput(InputOutput):
script_class = None
NULL_SIGNATURE = '0'*72 NULL_SIGNATURE = '0'*72
NULL_PUBLIC_KEY = '0'*33 NULL_PUBLIC_KEY = '0'*33
def __init__(self, output_or_txid_index, script, sequence=0xFFFFFFFF): def __init__(self, output_or_txid_index, script, sequence=0xFFFFFFFF):
if isinstance(output_or_txid_index, Output): if isinstance(output_or_txid_index, BaseOutput):
self.output = output_or_txid_index # type: Output self.output = output_or_txid_index # type: BaseOutput
self.output_txid = self.output.transaction.hash self.output_txid = self.output.transaction.hash
self.output_index = self.output.index self.output_index = self.output.index
else: else:
self.output = None # type: Output self.output = None # type: BaseOutput
self.output_txid, self.output_index = output_or_txid_index self.output_txid, self.output_index = output_or_txid_index
self.sequence = sequence self.sequence = sequence
self.is_coinbase = self.output_txid == NULL_HASH self.is_coinbase = self.output_txid == NULL_HASH
self.coinbase = script if self.is_coinbase else None self.coinbase = script if self.is_coinbase else None
self.script = script if not self.is_coinbase else None # type: InputScript self.script = script if not self.is_coinbase else None # type: BaseInputScript
def link_output(self, output): def link_output(self, output):
assert self.output is None assert self.output is None
assert self.output_txid == output.transaction.id assert self.output_txid == output.transaction.hash
assert self.output_index == output.index assert self.output_index == output.index
self.output = output self.output = output
@classmethod
def spend(cls, output):
""" Create an input to spend the output."""
assert output.script.is_pay_pubkey_hash, 'Attempting to spend unsupported output.'
script = cls.script_class.redeem_pubkey_hash(cls.NULL_SIGNATURE, cls.NULL_PUBLIC_KEY)
return cls(output, script)
@property @property
def amount(self): def amount(self):
""" Amount this input adds to the transaction. """ """ Amount this input adds to the transaction. """
@ -82,7 +84,7 @@ class Input(InputOutput):
sequence = stream.read_uint32() sequence = stream.read_uint32()
return cls( return cls(
(txid, index), (txid, index),
InputScript(script) if not txid == NULL_HASH else script, cls.script_class(script) if not txid == NULL_HASH else script,
sequence sequence
) )
@ -98,86 +100,48 @@ class Input(InputOutput):
stream.write_string(self.script.source) stream.write_string(self.script.source)
stream.write_uint32(self.sequence) 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 BaseOutput(InputOutput):
class Output(InputOutput): script_class = None
def __init__(self, transaction, index, amount, script): def __init__(self, amount, script):
self.transaction = transaction # type: Transaction
self.index = index # type: int
self.amount = amount # type: int self.amount = amount # type: int
self.script = script # type: OutputScript self.script = script # type: BaseOutputScript
self.transaction = None # type: BaseTransaction
self.index = None # type: int
self._effective_amount = None # type: int self._effective_amount = None # type: int
def __lt__(self, other): def __lt__(self, other):
return self.effective_amount < other.effective_amount return self.effective_amount < other.effective_amount
def _add_and_return(self):
self.transaction.add_outputs([self])
return self
@classmethod @classmethod
def pay_pubkey_hash(cls, transaction, index, amount, pubkey_hash): def pay_pubkey_hash(cls, amount, pubkey_hash):
return cls( return cls(amount, cls.script_class.pay_pubkey_hash(pubkey_hash))
transaction, index, amount,
OutputScript.pay_pubkey_hash(pubkey_hash)
)._add_and_return()
@classmethod
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()
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 @property
def effective_amount(self): def effective_amount(self):
""" Amount minus fees it would take to spend this output. """ """ Amount minus fees it would take to spend this output. """
if self._effective_amount is None: if self._effective_amount is None:
txi = self.spend() self._effective_amount = self.input_class.spend(self).effective_amount
self._effective_amount = txi.effective_amount
return self._effective_amount return self._effective_amount
@classmethod @classmethod
def deserialize_from(cls, stream, transaction, index): def deserialize_from(cls, stream):
return cls( return cls(
transaction=transaction,
index=index,
amount=stream.read_uint64(), amount=stream.read_uint64(),
script=OutputScript(stream.read_string()) script=cls.script_class(stream.read_string())
) )
def serialize_to(self, stream): def serialize_to(self, stream):
stream.write_uint64(self.amount) stream.write_uint64(self.amount)
stream.write_string(self.script.source) stream.write_string(self.script.source)
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))
class BaseTransaction:
class Transaction: input_class = None
output_class = None
def __init__(self, raw=None, version=1, locktime=0, height=None, is_saved=False): def __init__(self, raw=None, version=1, locktime=0, height=None, is_saved=False):
self._raw = raw self._raw = raw
@ -186,8 +150,8 @@ class Transaction:
self.version = version # type: int self.version = version # type: int
self.locktime = locktime # type: int self.locktime = locktime # type: int
self.height = height # type: int self.height = height # type: int
self.inputs = [] # type: List[Input] self._inputs = [] # type: List[BaseInput]
self.outputs = [] # type: List[Output] self._outputs = [] # type: List[BaseOutput]
self.is_saved = is_saved # type: bool self.is_saved = is_saved # type: bool
if raw is not None: if raw is not None:
self._deserialize() self._deserialize()
@ -211,19 +175,30 @@ class Transaction:
return self._raw return self._raw
def _reset(self): def _reset(self):
self._raw = None
self._hash = None
self._id = None self._id = None
self._hash = None
def get_claim_id(self, output_index): self._raw = None
script = self.outputs[output_index]
assert script.script.is_claim_name(), 'Not a name claim.'
return claim_id_hash(self.hash, output_index)
@property @property
def is_complete(self): def inputs(self): # type: () -> ReadOnlyList[BaseInput]
s, r = self.signature_count() return ReadOnlyList(self._inputs)
return r == s
@property
def outputs(self): # type: () -> ReadOnlyList[BaseOutput]
return ReadOnlyList(self._outputs)
def add_inputs(self, inputs):
self._inputs.extend(inputs)
self._reset()
return self
def add_outputs(self, outputs):
for txo in outputs:
txo.transaction = self
txo.index = len(self._outputs)
self._outputs.append(txo)
self._reset()
return self
@property @property
def fee(self): def fee(self):
@ -240,30 +215,15 @@ class Transaction:
""" Size in bytes of transaction meta data and all outputs; without inputs. """ """ Size in bytes of transaction meta data and all outputs; without inputs. """
return len(self._serialize(with_inputs=False)) return len(self._serialize(with_inputs=False))
@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): def _serialize(self, with_inputs=True):
stream = BCDataStream() stream = BCDataStream()
stream.write_uint32(self.version) stream.write_uint32(self.version)
if with_inputs: if with_inputs:
stream.write_compact_size(len(self.inputs)) stream.write_compact_size(len(self._inputs))
for txin in self.inputs: for txin in self._inputs:
txin.serialize_to(stream) txin.serialize_to(stream)
stream.write_compact_size(len(self.outputs)) stream.write_compact_size(len(self._outputs))
for txout in self.outputs: for txout in self._outputs:
txout.serialize_to(stream) txout.serialize_to(stream)
stream.write_uint32(self.locktime) stream.write_uint32(self.locktime)
return stream.get_bytes() return stream.get_bytes()
@ -271,14 +231,14 @@ class Transaction:
def _serialize_for_signature(self, signing_input): def _serialize_for_signature(self, signing_input):
stream = BCDataStream() stream = BCDataStream()
stream.write_uint32(self.version) stream.write_uint32(self.version)
stream.write_compact_size(len(self.inputs)) stream.write_compact_size(len(self._inputs))
for i, txin in enumerate(self.inputs): for i, txin in enumerate(self._inputs):
if signing_input == i: if signing_input == i:
txin.serialize_to(stream, txin.output.script.source) txin.serialize_to(stream, txin.output.script.source)
else: else:
txin.serialize_to(stream, b'') txin.serialize_to(stream, b'')
stream.write_compact_size(len(self.outputs)) stream.write_compact_size(len(self._outputs))
for txout in self.outputs: for txout in self._outputs:
txout.serialize_to(stream) txout.serialize_to(stream)
stream.write_uint32(self.locktime) stream.write_uint32(self.locktime)
stream.write_uint32(1) # signature hash type: SIGHASH_ALL stream.write_uint32(1) # signature hash type: SIGHASH_ALL
@ -289,58 +249,37 @@ class Transaction:
stream = BCDataStream(self._raw) stream = BCDataStream(self._raw)
self.version = stream.read_uint32() self.version = stream.read_uint32()
input_count = stream.read_compact_size() input_count = stream.read_compact_size()
self.inputs = [Input.deserialize_from(stream) for _ in range(input_count)] self.add_inputs([
self.input_class.deserialize_from(stream) for _ in range(input_count)
])
output_count = stream.read_compact_size() output_count = stream.read_compact_size()
self.outputs = [Output.deserialize_from(stream, self, i) for i in range(output_count)] self.add_outputs([
self.output_class.deserialize_from(stream) for _ in range(output_count)
])
self.locktime = stream.read_uint32() self.locktime = stream.read_uint32()
def add_inputs(self, inputs): def sign(self, account): # type: (Account) -> BaseTransaction
self.inputs.extend(inputs) for i, txi in enumerate(self._inputs):
self._reset()
def add_outputs(self, outputs):
self.outputs.extend(outputs)
self._reset()
def sign(self, wallet): # type: (Wallet) -> bool
for i, txi in enumerate(self.inputs):
txo_script = txi.output.script txo_script = txi.output.script
if txo_script.is_pay_pubkey_hash: if txo_script.is_pay_pubkey_hash:
address = hash160_to_address(txo_script.values['pubkey_hash'], wallet.chain) address = account.coin.hash160_to_address(txo_script.values['pubkey_hash'])
private_key = wallet.get_private_key_for_address(address) private_key = account.get_private_key_for_address(address)
tx = self._serialize_for_signature(i) tx = self._serialize_for_signature(i)
txi.script.values['signature'] = private_key.sign(tx)+six.int2byte(1) txi.script.values['signature'] = private_key.sign(tx)+six.int2byte(1)
txi.script.values['pubkey'] = private_key.public_key.pubkey_bytes txi.script.values['pubkey'] = private_key.public_key.pubkey_bytes
txi.script.generate() txi.script.generate()
self._reset() self._reset()
return True return self
def sort(self): def sort(self):
# See https://github.com/kristovatlas/rfc/blob/master/bips/bip-li01.mediawiki # 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._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]))) self._outputs.sort(key=lambda o: (o[2], pay_script(o[0], o[1])))
@property @property
def input_sum(self): def input_sum(self):
return sum(i.amount for i in self.inputs) return sum(i.amount for i in self._inputs)
@property @property
def output_sum(self): def output_sum(self):
return sum(o.amount for o in self.outputs) 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

@ -10,14 +10,14 @@
import struct import struct
import hashlib import hashlib
from binascii import unhexlify
from six import int2byte, byte2int from six import int2byte, byte2int
import ecdsa import ecdsa
import ecdsa.ellipticcurve as EC import ecdsa.ellipticcurve as EC
import ecdsa.numbertheory as NT import ecdsa.numbertheory as NT
from .hash import Base58, hmac_sha512, hash160, double_sha256, public_key_to_address from .basecoin import BaseCoin
from .hash import Base58, hmac_sha512, hash160, double_sha256
from .util import cachedproperty, bytes_to_int, int_to_bytes from .util import cachedproperty, bytes_to_int, int_to_bytes
@ -30,7 +30,9 @@ class _KeyBase(object):
CURVE = ecdsa.SECP256k1 CURVE = ecdsa.SECP256k1
def __init__(self, chain_code, n, depth, parent): def __init__(self, coin, chain_code, n, depth, parent):
if not isinstance(coin, BaseCoin):
raise TypeError('invalid coin')
if not isinstance(chain_code, (bytes, bytearray)): if not isinstance(chain_code, (bytes, bytearray)):
raise TypeError('chain code must be raw bytes') raise TypeError('chain code must be raw bytes')
if len(chain_code) != 32: if len(chain_code) != 32:
@ -42,6 +44,7 @@ class _KeyBase(object):
if parent is not None: if parent is not None:
if not isinstance(parent, type(self)): if not isinstance(parent, type(self)):
raise TypeError('parent key has bad type') raise TypeError('parent key has bad type')
self.coin = coin
self.chain_code = chain_code self.chain_code = chain_code
self.n = n self.n = n
self.depth = depth self.depth = depth
@ -83,8 +86,8 @@ class _KeyBase(object):
class PubKey(_KeyBase): class PubKey(_KeyBase):
""" A BIP32 public key. """ """ A BIP32 public key. """
def __init__(self, pubkey, chain_code, n, depth, parent=None): def __init__(self, coin, pubkey, chain_code, n, depth, parent=None):
super(PubKey, self).__init__(chain_code, n, depth, parent) super(PubKey, self).__init__(coin, chain_code, n, depth, parent)
if isinstance(pubkey, ecdsa.VerifyingKey): if isinstance(pubkey, ecdsa.VerifyingKey):
self.verifying_key = pubkey self.verifying_key = pubkey
else: else:
@ -126,7 +129,7 @@ class PubKey(_KeyBase):
@cachedproperty @cachedproperty
def address(self): def address(self):
""" The public key as a P2PKH address. """ """ The public key as a P2PKH address. """
return public_key_to_address(self.pubkey_bytes, 'regtest') return self.coin.public_key_to_address(self.pubkey_bytes)
def ec_point(self): def ec_point(self):
return self.verifying_key.pubkey.point return self.verifying_key.pubkey.point
@ -150,7 +153,7 @@ class PubKey(_KeyBase):
verkey = ecdsa.VerifyingKey.from_public_point(point, curve=curve) verkey = ecdsa.VerifyingKey.from_public_point(point, curve=curve)
return PubKey(verkey, R, n, self.depth + 1, self) return PubKey(self.coin, verkey, R, n, self.depth + 1, self)
def identifier(self): def identifier(self):
""" Return the key's identifier as 20 bytes. """ """ Return the key's identifier as 20 bytes. """
@ -158,7 +161,10 @@ class PubKey(_KeyBase):
def extended_key(self): def extended_key(self):
""" Return a raw extended public key. """ """ Return a raw extended public key. """
return self._extended_key(unhexlify("0488b21e"), self.pubkey_bytes) return self._extended_key(
self.coin.extended_public_key_prefix,
self.pubkey_bytes
)
class LowSValueSigningKey(ecdsa.SigningKey): class LowSValueSigningKey(ecdsa.SigningKey):
@ -180,8 +186,8 @@ class PrivateKey(_KeyBase):
HARDENED = 1 << 31 HARDENED = 1 << 31
def __init__(self, privkey, chain_code, n, depth, parent=None): def __init__(self, coin, privkey, chain_code, n, depth, parent=None):
super(PrivateKey, self).__init__(chain_code, n, depth, parent) super(PrivateKey, self).__init__(coin, chain_code, n, depth, parent)
if isinstance(privkey, ecdsa.SigningKey): if isinstance(privkey, ecdsa.SigningKey):
self.signing_key = privkey self.signing_key = privkey
else: else:
@ -206,11 +212,11 @@ class PrivateKey(_KeyBase):
return exponent return exponent
@classmethod @classmethod
def from_seed(cls, seed): def from_seed(cls, coin, seed):
# This hard-coded message string seems to be coin-independent... # This hard-coded message string seems to be coin-independent...
hmac = hmac_sha512(b'Bitcoin seed', seed) hmac = hmac_sha512(b'Bitcoin seed', seed)
privkey, chain_code = hmac[:32], hmac[32:] privkey, chain_code = hmac[:32], hmac[32:]
return cls(privkey, chain_code, 0, 0) return cls(coin, privkey, chain_code, 0, 0)
@cachedproperty @cachedproperty
def private_key_bytes(self): def private_key_bytes(self):
@ -222,7 +228,7 @@ class PrivateKey(_KeyBase):
""" Return the corresponding extended public key. """ """ Return the corresponding extended public key. """
verifying_key = self.signing_key.get_verifying_key() verifying_key = self.signing_key.get_verifying_key()
parent_pubkey = self.parent.public_key if self.parent else None parent_pubkey = self.parent.public_key if self.parent else None
return PubKey(verifying_key, self.chain_code, self.n, self.depth, return PubKey(self.coin, verifying_key, self.chain_code, self.n, self.depth,
parent_pubkey) parent_pubkey)
def ec_point(self): def ec_point(self):
@ -234,7 +240,7 @@ class PrivateKey(_KeyBase):
def wif(self): def wif(self):
""" Return the private key encoded in Wallet Import Format. """ """ Return the private key encoded in Wallet Import Format. """
return b'\x1c' + self.private_key_bytes + b'\x01' return self.coin.private_key_to_wif(self.private_key_bytes)
def address(self): def address(self):
""" The public key as a P2PKH address. """ """ The public key as a P2PKH address. """
@ -261,7 +267,7 @@ class PrivateKey(_KeyBase):
privkey = _exponent_to_bytes(exponent) privkey = _exponent_to_bytes(exponent)
return PrivateKey(privkey, R, n, self.depth + 1, self) return PrivateKey(self.coin, privkey, R, n, self.depth + 1, self)
def sign(self, data): def sign(self, data):
""" Produce a signature for piece of data by double hashing it and signing the hash. """ """ Produce a signature for piece of data by double hashing it and signing the hash. """
@ -275,7 +281,10 @@ class PrivateKey(_KeyBase):
def extended_key(self): def extended_key(self):
"""Return a raw extended private key.""" """Return a raw extended private key."""
return self._extended_key(unhexlify("0488ade4"), b'\0' + self.private_key_bytes) return self._extended_key(
self.coin.extended_private_key_prefix,
b'\0' + self.private_key_bytes
)
def _exponent_to_bytes(exponent): def _exponent_to_bytes(exponent):
@ -283,7 +292,7 @@ def _exponent_to_bytes(exponent):
return (int2byte(0)*32 + int_to_bytes(exponent))[-32:] return (int2byte(0)*32 + int_to_bytes(exponent))[-32:]
def _from_extended_key(ekey): def _from_extended_key(coin, ekey):
"""Return a PubKey or PrivateKey from an extended key raw bytes.""" """Return a PubKey or PrivateKey from an extended key raw bytes."""
if not isinstance(ekey, (bytes, bytearray)): if not isinstance(ekey, (bytes, bytearray)):
raise TypeError('extended key must be raw bytes') raise TypeError('extended key must be raw bytes')
@ -295,21 +304,21 @@ def _from_extended_key(ekey):
n, = struct.unpack('>I', ekey[9:13]) n, = struct.unpack('>I', ekey[9:13])
chain_code = ekey[13:45] chain_code = ekey[13:45]
if ekey[:4] == unhexlify("0488b21e"): if ekey[:4] == coin.extended_public_key_prefix:
pubkey = ekey[45:] pubkey = ekey[45:]
key = PubKey(pubkey, chain_code, n, depth) key = PubKey(coin, pubkey, chain_code, n, depth)
elif ekey[:4] == unhexlify("0488ade4"): elif ekey[:4] == coin.extended_private_key_prefix:
if ekey[45] is not int2byte(0): if ekey[45] is not int2byte(0):
raise ValueError('invalid extended private key prefix byte') raise ValueError('invalid extended private key prefix byte')
privkey = ekey[46:] privkey = ekey[46:]
key = PrivateKey(privkey, chain_code, n, depth) key = PrivateKey(coin, privkey, chain_code, n, depth)
else: else:
raise ValueError('version bytes unrecognised') raise ValueError('version bytes unrecognised')
return key return key
def from_extended_key_string(ekey_str): def from_extended_key_string(coin, ekey_str):
"""Given an extended key string, such as """Given an extended key string, such as
xpub6BsnM1W2Y7qLMiuhi7f7dbAwQZ5Cz5gYJCRzTNainXzQXYjFwtuQXHd xpub6BsnM1W2Y7qLMiuhi7f7dbAwQZ5Cz5gYJCRzTNainXzQXYjFwtuQXHd
@ -317,4 +326,4 @@ def from_extended_key_string(ekey_str):
return a PubKey or PrivateKey. return a PubKey or PrivateKey.
""" """
return _from_extended_key(Base58.decode_check(ekey_str)) return _from_extended_key(coin, Base58.decode_check(ekey_str))

View file

@ -0,0 +1,2 @@
from . import lbc
from . import bitcoin

View file

@ -0,0 +1,43 @@
from six import int2byte
from binascii import unhexlify
from lbrynet.wallet.baseledger import BaseLedger
from lbrynet.wallet.basenetwork import BaseNetwork
from lbrynet.wallet.basescript import BaseInputScript, BaseOutputScript
from lbrynet.wallet.basetransaction import BaseTransaction, BaseInput, BaseOutput
from lbrynet.wallet.basecoin import BaseCoin
class Ledger(BaseLedger):
network_class = BaseNetwork
class Input(BaseInput):
script_class = BaseInputScript
class Output(BaseOutput):
script_class = BaseOutputScript
class Transaction(BaseTransaction):
input_class = BaseInput
output_class = BaseOutput
class BTC(BaseCoin):
name = 'Bitcoin'
symbol = 'BTC'
network = 'mainnet'
ledger_class = Ledger
transaction_class = Transaction
pubkey_address_prefix = int2byte(0x00)
script_address_prefix = int2byte(0x05)
extended_public_key_prefix = unhexlify('0488b21e')
extended_private_key_prefix = unhexlify('0488ade4')
default_fee_per_byte = 50
def __init__(self, ledger, fee_per_byte=default_fee_per_byte):
super(BTC, self).__init__(ledger, fee_per_byte)

View file

@ -0,0 +1 @@
from .coin import LBC, LBCTestNet, LBCRegTest

View file

@ -0,0 +1,67 @@
from six import int2byte
from binascii import unhexlify
from lbrynet.wallet.basecoin import BaseCoin
from .ledger import MainNetLedger, TestNetLedger, RegTestLedger
from .transaction import Transaction
class LBC(BaseCoin):
name = 'LBRY Credits'
symbol = 'LBC'
network = 'mainnet'
ledger_class = MainNetLedger
transaction_class = Transaction
secret_prefix = int2byte(0x1c)
pubkey_address_prefix = int2byte(0x55)
script_address_prefix = int2byte(0x7a)
extended_public_key_prefix = unhexlify('019c354f')
extended_private_key_prefix = unhexlify('019c3118')
default_fee_per_byte = 50
default_fee_per_name_char = 200000
def __init__(self, ledger, fee_per_byte=default_fee_per_byte,
fee_per_name_char=default_fee_per_name_char):
super(LBC, self).__init__(ledger, fee_per_byte)
self.fee_per_name_char = fee_per_name_char
def to_dict(self):
coin_dict = super(LBC, self).to_dict()
coin_dict['fee_per_name_char'] = self.fee_per_name_char
return coin_dict
def get_transaction_base_fee(self, tx):
""" Fee for the transaction header and all outputs; without inputs. """
return max(
super(LBC, self).get_transaction_base_fee(tx),
self.get_transaction_claim_name_fee(tx)
)
def get_transaction_claim_name_fee(self, tx):
fee = 0
for output in tx.outputs:
if output.script.is_claim_name:
fee += len(output.script.values['claim_name']) * self.fee_per_name_char
return fee
class LBCTestNet(LBC):
network = 'testnet'
ledger_class = TestNetLedger
pubkey_address_prefix = int2byte(111)
script_address_prefix = int2byte(196)
extended_public_key_prefix = unhexlify('043587cf')
extended_private_key_prefix = unhexlify('04358394')
class LBCRegTest(LBC):
network = 'regtest'
ledger_class = RegTestLedger
pubkey_address_prefix = int2byte(111)
script_address_prefix = int2byte(196)
extended_public_key_prefix = unhexlify('043587cf')
extended_private_key_prefix = unhexlify('04358394')

View file

@ -0,0 +1,28 @@
from lbrynet.wallet.baseledger import BaseLedger
from .network import Network
class LBCLedger(BaseLedger):
network_class = Network
header_size = 112
max_target = 0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
genesis_hash = '9c89283ba0f3227f6c03b70216b9f665f0118d5e0fa729cedf4fb34d6a34f463'
genesis_bits = 0x1f00ffff
target_timespan = 150
class MainNetLedger(LBCLedger):
pass
class TestNetLedger(LBCLedger):
pass
class RegTestLedger(LBCLedger):
max_target = 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
genesis_hash = '6e3fcf1299d4ec5d79c3a4c91d624a4acf9e2e173d95a1a0504f677669687556'
genesis_bits = 0x207fffff
target_timespan = 1
verify_bits_to_target = False

View file

@ -0,0 +1,5 @@
from lbrynet.wallet.basenetwork import BaseNetwork
class Network(BaseNetwork):
pass

View file

@ -0,0 +1,80 @@
from lbrynet.wallet.basescript import BaseInputScript, BaseOutputScript, Template
from lbrynet.wallet.basescript import PUSH_SINGLE, OP_DROP, OP_2DROP
class InputScript(BaseInputScript):
pass
class OutputScript(BaseOutputScript):
# lbry custom opcodes
OP_CLAIM_NAME = 0xb5
OP_SUPPORT_CLAIM = 0xb6
OP_UPDATE_CLAIM = 0xb7
CLAIM_NAME_OPCODES = (
OP_CLAIM_NAME, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim'),
OP_2DROP, OP_DROP
)
CLAIM_NAME_PUBKEY = Template('claim_name+pay_pubkey_hash', (
CLAIM_NAME_OPCODES + BaseOutputScript.PAY_PUBKEY_HASH.opcodes
))
CLAIM_NAME_SCRIPT = Template('claim_name+pay_script_hash', (
CLAIM_NAME_OPCODES + BaseOutputScript.PAY_SCRIPT_HASH.opcodes
))
SUPPORT_CLAIM_OPCODES = (
OP_SUPPORT_CLAIM, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim_id'),
OP_2DROP, OP_DROP
)
SUPPORT_CLAIM_PUBKEY = Template('support_claim+pay_pubkey_hash', (
SUPPORT_CLAIM_OPCODES + BaseOutputScript.PAY_PUBKEY_HASH.opcodes
))
SUPPORT_CLAIM_SCRIPT = Template('support_claim+pay_script_hash', (
SUPPORT_CLAIM_OPCODES + BaseOutputScript.PAY_SCRIPT_HASH.opcodes
))
UPDATE_CLAIM_OPCODES = (
OP_UPDATE_CLAIM, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim_id'), PUSH_SINGLE('claim'),
OP_2DROP, OP_2DROP
)
UPDATE_CLAIM_PUBKEY = Template('update_claim+pay_pubkey_hash', (
UPDATE_CLAIM_OPCODES + BaseOutputScript.PAY_PUBKEY_HASH.opcodes
))
UPDATE_CLAIM_SCRIPT = Template('update_claim+pay_script_hash', (
UPDATE_CLAIM_OPCODES + BaseOutputScript.PAY_SCRIPT_HASH.opcodes
))
templates = BaseOutputScript.templates + [
CLAIM_NAME_PUBKEY,
CLAIM_NAME_SCRIPT,
SUPPORT_CLAIM_PUBKEY,
SUPPORT_CLAIM_SCRIPT,
UPDATE_CLAIM_PUBKEY,
UPDATE_CLAIM_SCRIPT
]
@classmethod
def pay_claim_name_pubkey_hash(cls, claim_name, claim, pubkey_hash):
return cls(template=cls.CLAIM_NAME_PUBKEY, values={
'claim_name': claim_name,
'claim': claim,
'pubkey_hash': pubkey_hash
})
@property
def is_claim_name(self):
return self.template.name.startswith('claim_name+')
@property
def is_support_claim(self):
return self.template.name.startswith('support_claim+')
@property
def is_update_claim(self):
return self.template.name.startswith('update_claim+')
@property
def is_claim_involved(self):
return self.is_claim_name or self.is_support_claim or self.is_update_claim

View file

@ -0,0 +1,34 @@
import struct
from lbrynet.wallet.basetransaction import BaseTransaction, BaseInput, BaseOutput
from lbrynet.wallet.hash import hash160
from .script import InputScript, OutputScript
def claim_id_hash(txid, n):
return hash160(txid + struct.pack('>I', n))
class Input(BaseInput):
script_class = InputScript
class Output(BaseOutput):
script_class = OutputScript
@classmethod
def pay_claim_name_pubkey_hash(cls, amount, claim_name, claim, pubkey_hash):
script = cls.script_class.pay_claim_name_pubkey_hash(claim_name, claim, pubkey_hash)
return cls(amount, script)
class Transaction(BaseTransaction):
input_class = Input
output_class = Output
def get_claim_id(self, output_index):
output = self._outputs[output_index]
assert output.script.is_claim_name(), 'Not a name claim.'
return claim_id_hash(self.hash, output_index)

View file

@ -1,5 +1,3 @@
from lbrynet import __version__
LBRYUM_VERSION = __version__
PROTOCOL_VERSION = '0.10' # protocol version requested PROTOCOL_VERSION = '0.10' # protocol version requested
NEW_SEED_VERSION = 11 # lbryum versions >= 2.0 NEW_SEED_VERSION = 11 # lbryum versions >= 2.0
OLD_SEED_VERSION = 4 # lbryum versions < 2.0 OLD_SEED_VERSION = 4 # lbryum versions < 2.0
@ -9,73 +7,19 @@ SEED_PREFIX = '01' # Electrum standard wallet
SEED_PREFIX_2FA = '101' # extended seed for two-factor authentication SEED_PREFIX_2FA = '101' # extended seed for two-factor authentication
MAXIMUM_FEE_PER_BYTE = 50
MAXIMUM_FEE_PER_NAME_CHAR = 200000
COINBASE_MATURITY = 100 COINBASE_MATURITY = 100
CENT = 1000000 CENT = 1000000
COIN = 100*CENT COIN = 100*CENT
# supported types of transaction outputs
TYPE_ADDRESS = 1
TYPE_PUBKEY = 2
TYPE_SCRIPT = 4
TYPE_CLAIM = 8
TYPE_SUPPORT = 16
TYPE_UPDATE = 32
# claim related constants
EXPIRATION_BLOCKS = 262974
RECOMMENDED_CLAIMTRIE_HASH_CONFIRMS = 1 RECOMMENDED_CLAIMTRIE_HASH_CONFIRMS = 1
NO_SIGNATURE = 'ff' NO_SIGNATURE = 'ff'
NULL_HASH = '0000000000000000000000000000000000000000000000000000000000000000' NULL_HASH = '0000000000000000000000000000000000000000000000000000000000000000'
HEADER_SIZE = 112
BLOCKS_PER_CHUNK = 96
CLAIM_ID_SIZE = 20 CLAIM_ID_SIZE = 20
HEADERS_URL = "https://s3.amazonaws.com/lbry-blockchain-headers/blockchain_headers_latest"
DEFAULT_PORTS = {'t': '50001', 's': '50002', 'h': '8081', 'g': '8082'} DEFAULT_PORTS = {'t': '50001', 's': '50002', 'h': '8081', 'g': '8082'}
NODES_RETRY_INTERVAL = 60 NODES_RETRY_INTERVAL = 60
SERVER_RETRY_INTERVAL = 10 SERVER_RETRY_INTERVAL = 10
MAX_BATCH_QUERY_SIZE = 500 MAX_BATCH_QUERY_SIZE = 500
proxy_modes = ['socks4', 'socks5', 'http'] proxy_modes = ['socks4', 'socks5', 'http']
# 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,
'script_address_prefix': 122,
'genesis_hash': '9c89283ba0f3227f6c03b70216b9f665f0118d5e0fa729cedf4fb34d6a34f463',
'max_target': 0x0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,
'genesis_bits': 0x1f00ffff,
'target_timespan': 150
},
TESTNET_CHAIN: {
'pubkey_address': 0,
'script_address': 5,
'pubkey_address_prefix': 111,
'script_address_prefix': 196,
'genesis_hash': '9c89283ba0f3227f6c03b70216b9f665f0118d5e0fa729cedf4fb34d6a34f463',
'max_target': 0x0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,
'genesis_bits': 0x1f00ffff,
'target_timespan': 150
},
REGTEST_CHAIN: {
'pubkey_address': 0,
'script_address': 5,
'pubkey_address_prefix': 111,
'script_address_prefix': 196,
'genesis_hash': '6e3fcf1299d4ec5d79c3a4c91d624a4acf9e2e173d95a1a0504f677669687556',
'max_target': 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,
'genesis_bits': 0x207fffff,
'target_timespan': 1
}
}

View file

@ -13,11 +13,9 @@ import aes
import base64 import base64
import hashlib import hashlib
import hmac import hmac
import struct
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
from .util import bytes_to_int, int_to_bytes from .util import bytes_to_int, int_to_bytes
from .constants import CHAINS, MAIN_CHAIN
_sha256 = hashlib.sha256 _sha256 = hashlib.sha256
_sha512 = hashlib.sha512 _sha512 = hashlib.sha512
@ -77,26 +75,6 @@ def hex_str_to_hash(x):
return reversed(unhexlify(x)) 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): def aes_encrypt(secret, value):
return base64.b64encode(aes.encryptData(secret, value.encode('utf8'))) return base64.b64encode(aes.encryptData(secret, value.encode('utf8')))

View file

@ -1,141 +1,83 @@
import logging import functools
from binascii import unhexlify from typing import List, Dict, Type
from operator import itemgetter
from twisted.internet import defer from twisted.internet import defer
from lbrynet.wallet.wallet import Wallet from lbrynet.wallet.account import AccountsView
from lbrynet.wallet.ledger import Ledger from lbrynet.wallet.basecoin import CoinRegistry
from lbrynet.wallet.protocol import Network from lbrynet.wallet.baseledger import BaseLedger
from lbrynet.wallet.transaction import Transaction from lbrynet.wallet.wallet import Wallet, WalletStorage
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: class WalletManager:
def __init__(self, config=None, wallet=None, ledger=None, network=None): def __init__(self, wallets=None, ledgers=None):
self.config = config or {} self.wallets = wallets or [] # type: List[Wallet]
self.ledger = ledger or Ledger(self.config) self.ledgers = ledgers or {} # type: Dict[Type[BaseLedger],BaseLedger]
self.wallet = wallet or Wallet() self.running = False
self.wallets = [self.wallet]
self.network = network or Network(self.config) @classmethod
self.network.on_header.listen(self.process_header) def from_config(cls, config):
self.network.on_status.listen(self.process_status) wallets = []
manager = cls(wallets)
for coin_id, ledger_config in config.get('ledgers', {}).items():
manager.get_or_create_ledger(coin_id, ledger_config)
for wallet_path in config.get('wallets', []):
wallet_storage = WalletStorage(wallet_path)
wallet = Wallet.from_storage(wallet_storage, manager)
wallets.append(wallet)
return manager
def get_or_create_ledger(self, coin_id, ledger_config=None):
coin_class = CoinRegistry.get_coin_class(coin_id)
ledger_class = coin_class.ledger_class
ledger = self.ledgers.get(ledger_class)
if ledger is None:
ledger = ledger_class(self.get_accounts_view(coin_class), ledger_config or {})
self.ledgers[ledger_class] = ledger
return ledger
@property @property
def fee_per_byte(self): def default_wallet(self):
return self.config.get('fee_per_byte', MAXIMUM_FEE_PER_BYTE)
@property
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 wallet in self.wallets:
for address in wallet.addresses: return wallet
if not self.ledger.has_address(address):
yield address
def get_least_used_receiving_address(self, max_transactions=1000): @property
return self._get_least_used_address( def default_account(self):
self.wallet.default_account.receiving_keys.addresses, for wallet in self.wallets:
self.wallet.default_account.receiving_keys, return wallet.default_account
max_transactions
def get_accounts(self, coin_class):
for wallet in self.wallets:
for account in wallet.accounts:
if account.coin.__class__ is coin_class:
yield account
def get_accounts_view(self, coin_class):
return AccountsView(
functools.partial(self.get_accounts, coin_class)
) )
def get_least_used_change_address(self, max_transactions=100): def create_wallet(self, path, coin_class):
return self._get_least_used_address( storage = WalletStorage(path)
self.wallet.default_account.change_keys.addresses, wallet = Wallet.from_storage(storage, self)
self.wallet.default_account.change_keys, self.wallets.append(wallet)
max_transactions self.create_account(wallet, coin_class)
) return wallet
def _get_least_used_address(self, addresses, sequence, max_transactions): def create_account(self, wallet, coin_class):
address = self.ledger.get_least_used_address(addresses, max_transactions) ledger = self.get_or_create_ledger(coin_class.get_id())
if address: return wallet.generate_account(ledger)
return address
address = sequence.generate_next_address()
self.subscribe_history(address)
return address
@defer.inlineCallbacks @defer.inlineCallbacks
def start(self): def start_ledgers(self):
first_connection = self.network.on_connected.first self.running = True
self.network.start() yield defer.DeferredList([
yield first_connection l.start() for l in self.ledgers.values()
self.ledger.headers.touch()
yield self.update_headers()
yield self.network.subscribe_headers()
yield self.update_wallet()
def stop(self):
return self.network.stop()
@execute_serially
@defer.inlineCallbacks
def update_headers(self):
while True:
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.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.ledger.headers):
# New header from network directly connects after the last local header.
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):
# 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.addresses_without_history)
while addresses:
yield defer.gatherResults([
self.update_history(a) for a in addresses
])
addresses = self.wallet.ensure_enough_addresses()
# By this point all of the addresses should be restored and we
# can now subscribe all of them to receive updates.
yield defer.gatherResults([
self.subscribe_history(address)
for address in self.wallet.addresses
]) ])
@defer.inlineCallbacks @defer.inlineCallbacks
def update_history(self, address): def stop_ledgers(self):
history = yield self.network.get_history(address) yield defer.DeferredList([
for hash in map(itemgetter('tx_hash'), history): l.stop() for l in self.ledgers.values()
transaction = self.ledger.get_transaction(hash) ])
if not transaction: self.running = False
raw = yield self.network.get_transaction(hash)
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.ledger.get_status(address):
self.update_history(address)
def process_status(self, response):
address, status = response
if status != self.ledger.get_status(address):
self.update_history(address)

View file

@ -1,4 +1,17 @@
from binascii import unhexlify, hexlify from binascii import unhexlify, hexlify
from collections import Sequence
class ReadOnlyList(Sequence):
def __init__(self, lst):
self.lst = lst
def __getitem__(self, key):
return self.lst[key]
def __len__(self):
return len(self.lst)
def subclass_tuple(name, base): def subclass_tuple(name, base):
@ -17,6 +30,15 @@ class cachedproperty(object):
return value return value
class classproperty(object):
def __init__(self, f):
self.f = f
def __get__(self, obj, owner):
return self.f(owner)
def bytes_to_int(be_bytes): def bytes_to_int(be_bytes):
""" Interprets a big-endian sequence of bytes as an integer. """ """ Interprets a big-endian sequence of bytes as an integer. """
return int(hexlify(be_bytes), 16) return int(hexlify(be_bytes), 16)

View file

@ -1,110 +1,150 @@
import stat import stat
import json import json
import os import os
from typing import List, Dict
from lbrynet.wallet.account import Account from lbrynet.wallet.account import Account
from lbrynet.wallet.constants import MAIN_CHAIN from lbrynet.wallet.basecoin import CoinRegistry, BaseCoin
from lbrynet.wallet.baseledger import BaseLedger
def inflate_coin(manager, coin_id, coin_dict):
# type: ('WalletManager', str, Dict) -> BaseCoin
coin_class = CoinRegistry.get_coin_class(coin_id)
ledger = manager.get_or_create_ledger(coin_id)
return coin_class(ledger, **coin_dict)
class Wallet: class Wallet:
""" The primary role of Wallet is to encapsulate a collection
of accounts (seed/private keys) and the spending rules / settings
for the coins attached to those accounts. Wallets are represented
by physical files on the filesystem.
"""
def __init__(self, **kwargs): def __init__(self, name='Wallet', coins=None, accounts=None, storage=None):
self.name = kwargs.get('name', 'Wallet') self.name = name
self.chain = kwargs.get('chain', MAIN_CHAIN) self.coins = coins or [] # type: List[BaseCoin]
self.accounts = kwargs.get('accounts') or {0: Account.generate()} self.accounts = accounts or [] # type: List[Account]
self.storage = storage or WalletStorage()
def get_or_create_coin(self, ledger, coin_dict=None): # type: (BaseLedger, Dict) -> BaseCoin
for coin in self.coins:
if coin.__class__ is ledger.coin_class:
return coin
coin = ledger.coin_class(ledger, **(coin_dict or {}))
self.coins.append(coin)
return coin
def generate_account(self, ledger): # type: (BaseLedger) -> Account
coin = self.get_or_create_coin(ledger)
account = Account.generate(coin)
self.accounts.append(account)
return account
@classmethod @classmethod
def from_json(cls, json_data): def from_storage(cls, storage, manager): # type: (WalletStorage, 'WalletManager') -> Wallet
if 'accounts' in json_data: json_dict = storage.read()
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 to_json(self): coins = {}
for coin_id, coin_dict in json_dict.get('coins', {}).items():
coins[coin_id] = inflate_coin(manager, coin_id, coin_dict)
accounts = []
for account_dict in json_dict.get('accounts', []):
coin_id = account_dict['coin']
coin = coins.get(coin_id)
if coin is None:
coin = coins[coin_id] = inflate_coin(manager, coin_id, {})
account = Account.from_dict(coin, account_dict)
accounts.append(account)
return cls(
name=json_dict.get('name', 'Wallet'),
coins=list(coins.values()),
accounts=accounts,
storage=storage
)
def to_dict(self):
return { return {
'name': self.name, 'name': self.name,
'chain': self.chain, 'coins': {c.get_id(): c.to_dict() for c in self.coins},
'accounts': { 'accounts': [a.to_dict() for a in self.accounts]
a_id: a.to_json() for
a_id, a in self.accounts.items()
}
} }
def save(self):
self.storage.write(self.to_dict())
@property @property
def default_account(self): def default_account(self):
return self.accounts.get(0, None) for account in self.accounts:
return account
@property def get_account_private_key_for_address(self, address):
def addresses(self): for account in self.accounts:
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) private_key = account.get_private_key_for_address(address)
if private_key is not None: if private_key is not None:
return private_key return account, private_key
class EphemeralWalletStorage(dict): class WalletStorage:
LATEST_VERSION = 2 LATEST_VERSION = 2
def save(self): DEFAULT = {
return json.dumps(self, indent=4, sort_keys=True) 'version': LATEST_VERSION,
'name': 'Wallet',
'coins': {},
'accounts': []
}
def upgrade(self): def __init__(self, path=None, default=None):
self.path = path
self._default = default or self.DEFAULT.copy()
@property
def default(self):
return self._default.copy()
def read(self):
if self.path and self.path.exists(self.path):
with open(self.path, "r") as f:
json_data = f.read()
json_dict = json.loads(json_data)
if json_dict.get('version') == self.LATEST_VERSION and \
set(json_dict) == set(self._default):
return json_dict
else:
return self.upgrade(json_dict)
else:
return self.default
@classmethod
def upgrade(cls, json_dict):
json_dict = json_dict.copy()
def _rename_property(old, new): def _rename_property(old, new):
if old in self: if old in json_dict:
old_value = self[old] json_dict[new] = json_dict[old]
del self[old] del json_dict[old]
if new not in self:
self[new] = old_value
if self.get('version', 1) == 1: # upgrade from version 1 to version 2 version = json_dict.pop('version', -1)
# TODO: `addr_history` should actually be imported into SQLStorage and removed from wallet.
if version == 1: # upgrade from version 1 to version 2
_rename_property('addr_history', 'history') _rename_property('addr_history', 'history')
_rename_property('use_encryption', 'encrypted') _rename_property('use_encryption', 'encrypted')
_rename_property('gap_limit', 'gap_limit_for_receiving') _rename_property('gap_limit', 'gap_limit_for_receiving')
self['version'] = 2
self.save() upgraded = cls.DEFAULT
upgraded.update(json_dict)
return json_dict
def write(self, json_dict):
class PermanentWalletStorage(EphemeralWalletStorage): json_data = json.dumps(json_dict, indent=4, sort_keys=True)
if self.path is None:
def __init__(self, *args, **kwargs): return json_data
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:
storage = cls()
storage.path = path
return storage
def save(self):
json_data = super(PermanentWalletStorage, self).save()
temp_path = "%s.tmp.%s" % (self.path, os.getpid()) temp_path = "%s.tmp.%s" % (self.path, os.getpid())
with open(temp_path, "w") as f: with open(temp_path, "w") as f:
@ -116,12 +156,9 @@ class PermanentWalletStorage(EphemeralWalletStorage):
mode = os.stat(self.path).st_mode mode = os.stat(self.path).st_mode
else: else:
mode = stat.S_IREAD | stat.S_IWRITE mode = stat.S_IREAD | stat.S_IWRITE
try: try:
os.rename(temp_path, self.path) os.rename(temp_path, self.path)
except: except:
os.remove(self.path) os.remove(self.path)
os.rename(temp_path, self.path) os.rename(temp_path, self.path)
os.chmod(self.path, mode) os.chmod(self.path, mode)
return json_data