import six import logging from typing import List, Iterable, Generator from binascii import hexlify from torba import baseledger from torba.basescript import BaseInputScript, BaseOutputScript from torba.coinselection import CoinSelector from torba.constants import COIN from torba.bcd_data_stream import BCDataStream from torba.hash import sha256 from torba.account import Account from torba.util import ReadOnlyList log = logging.getLogger() NULL_HASH = b'\x00'*32 class InputOutput(object): def __init__(self, txid): self._txid = txid # type: bytes self.transaction = None # type: BaseTransaction self.index = None # type: int @property def txid(self): if self._txid is None: self._txid = self.transaction.id return self._txid @property def size(self): """ Size of this input / output in bytes. """ stream = BCDataStream() self.serialize_to(stream) return len(stream.get_bytes()) def serialize_to(self, stream): raise NotImplemented class BaseInput(InputOutput): script_class = None NULL_SIGNATURE = b'\x00'*72 NULL_PUBLIC_KEY = b'\x00'*33 def __init__(self, output_or_txid_index, script, sequence=0xFFFFFFFF, txid=None): super(BaseInput, self).__init__(txid) if isinstance(output_or_txid_index, BaseOutput): self.output = output_or_txid_index # type: BaseOutput self.output_txid = self.output.transaction.id self.output_index = self.output.index else: self.output = None # type: BaseOutput self.output_txid, self.output_index = output_or_txid_index self.sequence = sequence self.is_coinbase = self.output_txid == NULL_HASH self.coinbase = script if self.is_coinbase else None self.script = script if not self.is_coinbase else None # type: BaseInputScript def link_output(self, output): assert self.output is None assert self.output_txid == output.transaction.id assert self.output_index == output.index self.output = output @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 def amount(self): """ Amount this input adds to the transaction. """ if self.output is None: raise ValueError('Cannot get input value without referenced output.') return self.output.amount @classmethod def deserialize_from(cls, stream): txid = stream.read(32) index = stream.read_uint32() script = stream.read_string() sequence = stream.read_uint32() return cls( (txid, index), cls.script_class(script) if not txid == NULL_HASH else script, sequence ) def serialize_to(self, stream, alternate_script=None): stream.write(self.output_txid) stream.write_uint32(self.output_index) if alternate_script is not None: stream.write_string(alternate_script) else: if self.is_coinbase: stream.write_string(self.coinbase) else: stream.write_string(self.script.source) stream.write_uint32(self.sequence) class BaseOutputEffectiveAmountEstimator(object): __slots__ = 'coin', 'txi', 'txo', 'fee', 'effective_amount' def __init__(self, ledger, txo): # type: (baseledger.BaseLedger, BaseOutput) -> None self.txo = txo self.txi = ledger.transaction_class.input_class.spend(txo) self.fee = ledger.get_input_output_fee(self.txi) self.effective_amount = txo.amount - self.fee def __lt__(self, other): return self.effective_amount < other.effective_amount class BaseOutput(InputOutput): script_class = None estimator_class = BaseOutputEffectiveAmountEstimator def __init__(self, amount, script, txid=None): super(BaseOutput, self).__init__(txid) self.amount = amount # type: int self.script = script # type: BaseOutputScript def get_estimator(self, ledger): return self.estimator_class(ledger, self) @classmethod def pay_pubkey_hash(cls, amount, pubkey_hash): return cls(amount, cls.script_class.pay_pubkey_hash(pubkey_hash)) @classmethod def deserialize_from(cls, stream): return cls( amount=stream.read_uint64(), script=cls.script_class(stream.read_string()) ) def serialize_to(self, stream): stream.write_uint64(self.amount) stream.write_string(self.script.source) class BaseTransaction: input_class = None output_class = None def __init__(self, raw=None, version=1, locktime=0): self._raw = raw self._hash = None self._id = None self.version = version # type: int self.locktime = locktime # type: int self._inputs = [] # type: List[BaseInput] self._outputs = [] # type: List[BaseOutput] if raw is not None: self._deserialize() @property def hex_id(self): return hexlify(self.id) @property def id(self): if self._id is None: self._id = self.hash[::-1] return self._id @property def hash(self): if self._hash is None: self._hash = sha256(sha256(self.raw)) return self._hash @property def raw(self): if self._raw is None: self._raw = self._serialize() return self._raw def _reset(self): self._id = None self._hash = None self._raw = None @property def inputs(self): # type: () -> ReadOnlyList[BaseInput] return ReadOnlyList(self._inputs) @property def outputs(self): # type: () -> ReadOnlyList[BaseOutput] return ReadOnlyList(self._outputs) def _add(self, new_ios, existing_ios): for txio in new_ios: txio.transaction = self txio.index = len(existing_ios) existing_ios.append(txio) self._reset() return self def add_inputs(self, inputs): return self._add(inputs, self._inputs) def add_outputs(self, outputs): return self._add(outputs, self._outputs) @property def fee(self): """ Fee that will actually be paid.""" return self.input_sum - self.output_sum @property def size(self): """ Size in bytes of the entire transaction. """ return len(self.raw) @property def base_size(self): """ Size in bytes of transaction meta data and all outputs; without inputs. """ return len(self._serialize(with_inputs=False)) def _serialize(self, with_inputs=True): stream = BCDataStream() stream.write_uint32(self.version) if with_inputs: stream.write_compact_size(len(self._inputs)) for txin in self._inputs: txin.serialize_to(stream) stream.write_compact_size(len(self._outputs)) for txout in self._outputs: txout.serialize_to(stream) stream.write_uint32(self.locktime) return stream.get_bytes() def _serialize_for_signature(self, signing_input): stream = BCDataStream() stream.write_uint32(self.version) stream.write_compact_size(len(self._inputs)) for i, txin in enumerate(self._inputs): if signing_input == i: txin.serialize_to(stream, txin.output.script.source) else: txin.serialize_to(stream, b'') stream.write_compact_size(len(self._outputs)) for txout in self._outputs: txout.serialize_to(stream) stream.write_uint32(self.locktime) stream.write_uint32(1) # signature hash type: SIGHASH_ALL return stream.get_bytes() def _deserialize(self): if self._raw is not None: stream = BCDataStream(self._raw) self.version = stream.read_uint32() input_count = stream.read_compact_size() self.add_inputs([ self.input_class.deserialize_from(stream) for _ in range(input_count) ]) output_count = stream.read_compact_size() self.add_outputs([ self.output_class.deserialize_from(stream) for _ in range(output_count) ]) self.locktime = stream.read_uint32() @classmethod def get_effective_amount_estimators(cls, funding_accounts): # type: (Iterable[Account]) -> Generator[BaseOutputEffectiveAmountEstimator] for account in funding_accounts: for utxo in account.coin.ledger.get_unspent_outputs(account): yield utxo.get_estimator(account.coin) @classmethod def ensure_all_have_same_ledger(cls, funding_accounts, change_account=None): # type: (Iterable[Account], Account) -> baseledger.BaseLedger ledger = None for account in funding_accounts: if ledger is None: ledger = account.coin.ledger if ledger != account.coin.ledger: raise ValueError( 'All funding accounts used to create a transaction must be on the same ledger.' ) if change_account is not None and change_account.coin.ledger != ledger: raise ValueError('Change account must use same ledger as funding accounts.') return ledger @classmethod def pay(cls, outputs, funding_accounts, change_account): """ Efficiently spend utxos from funding_accounts to cover the new outputs. """ tx = cls().add_outputs(outputs) ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account) amount = ledger.get_transaction_base_fee(tx) selector = CoinSelector( list(cls.get_effective_amount_estimators(funding_accounts)), amount, ledger.get_input_output_fee( cls.output_class.pay_pubkey_hash(COIN, NULL_HASH) ) ) spendables = selector.select() if not spendables: raise ValueError('Not enough funds to cover this transaction.') spent_sum = sum(s.effective_amount for s in spendables) if spent_sum > amount: change_address = change_account.get_least_used_change_address() change_hash160 = change_account.coin.address_to_hash160(change_address) change_amount = spent_sum - amount tx.add_outputs([cls.output_class.pay_pubkey_hash(change_amount, change_hash160)]) tx.add_inputs([s.txi for s in spendables]) tx.sign(funding_accounts) return tx @classmethod def liquidate(cls, assets, funding_accounts, change_account): """ Spend assets (utxos) supplementing with funding_accounts if fee is higher than asset value. """ def sign(self, funding_accounts): # type: (Iterable[Account]) -> BaseTransaction ledger = self.ensure_all_have_same_ledger(funding_accounts) for i, txi in enumerate(self._inputs): txo_script = txi.output.script if txo_script.is_pay_pubkey_hash: address = ledger.coin_class.hash160_to_address(txo_script.values['pubkey_hash']) account = ledger.accounts.get_account_for_address(address) private_key = account.get_private_key_for_address(address) tx = self._serialize_for_signature(i) txi.script.values['signature'] = private_key.sign(tx)+six.int2byte(1) txi.script.values['pubkey'] = private_key.public_key.pubkey_bytes txi.script.generate() else: raise NotImplementedError("Don't know how to spend this output.") self._reset() return self def sort(self): # See https://github.com/kristovatlas/rfc/blob/master/bips/bip-li01.mediawiki self._inputs.sort(key=lambda i: (i['prevout_hash'], i['prevout_n'])) self._outputs.sort(key=lambda o: (o[2], pay_script(o[0], o[1]))) @property def input_sum(self): return sum(i.amount for i in self._inputs) @property def output_sum(self): return sum(o.amount for o in self._outputs)